diff --git a/electron/index.html b/electron/index.html
index 8e4f957..a86d703 100644
--- a/electron/index.html
+++ b/electron/index.html
@@ -31,11 +31,11 @@
Processing...
+How would you like to save your metadata changes?
+Version: ${manifest.bundleVersion || 'N/A'}
-Author: ${manifest.bundleAuthor || 'N/A'}
+ ${createEditableField('Version', 'bundleVersion', manifest.bundleVersion || 'N/A')} + ${createEditableField('Author', 'bundleAuthor', manifest.bundleAuthor || 'N/A')}Bundled On: ${manifest.bundledOn || 'N/A'}
-Comment: ${manifest.bundleComment || 'N/A'}
+ ${createEditableField('Comment', 'bundleComment', manifest.bundleComment || 'N/A')} ${manifest.bundleAuthorKeyFingerprint ? `Author Key: ${manifest.bundleAuthorKeyFingerprint}
` : ''} ${manifest.bundleSignature ? `Signature: ${manifest.bundleSignature}
` : ''}+ ${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); + } +} diff --git a/electron/styles.css b/electron/styles.css index 3c8e17b..00148a9 100644 --- a/electron/styles.css +++ b/electron/styles.css @@ -370,6 +370,99 @@ body { vertical-align: middle; } +/* Inline Editing Styles */ +.editable-field { + position: relative; + display: flex; + align-items: center; + gap: 8px; +} + +.edit-icon { + background: none; + border: none; + cursor: pointer; + font-size: 14px; + padding: 4px; + border-radius: 4px; + transition: background-color 0.2s; + opacity: 0.6; +} + +.edit-icon:hover { + background-color: var(--border-color); + opacity: 1; +} + +.field-input { + padding: 4px 8px; + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 14px; + font-family: var(--font-family); + background-color: white; + min-width: 200px; +} + +.field-input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); +} + +.field-display { + flex: 1; +} + +.hidden { + display: none !important; +} + +/* Save Options Modal */ +.save-options { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 20px; +} + +.save-option-btn { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 16px; + border: 2px solid var(--border-color); + border-radius: 8px; + background: white; + cursor: pointer; + transition: all 0.2s; + text-align: left; +} + +.save-option-btn:hover { + border-color: var(--primary-color); + background-color: rgba(52, 152, 219, 0.05); +} + +.save-option-btn.primary { + border-color: var(--primary-color); +} + +.save-option-btn.secondary { + border-color: var(--text-light); +} + +.save-option-btn strong { + font-size: 16px; + margin-bottom: 4px; + color: var(--text-color); +} + +.save-option-btn small { + font-size: 14px; + color: var(--text-light); +} + .progress-indicator.spinner-icon { border: 2px solid var(--text-color-light); border-top: 2px solid var(--primary-color); diff --git a/fourdst/core/bundle.py b/fourdst/core/bundle.py index 23b9d65..cefefcb 100644 --- a/fourdst/core/bundle.py +++ b/fourdst/core/bundle.py @@ -283,10 +283,10 @@ def edit_bundle_metadata(bundle_path: Path, metadata: dict, progress_callback: O raise FileNotFoundError("Bundle is not a valid zip file.") with zipfile.ZipFile(bundle_path, 'a') as zf: - if MANIFEST_FILENAME not in zf.namelist(): - raise FileNotFoundError(f"{MANIFEST_FILENAME} not found in bundle.") + if "manifest.yaml" not in zf.namelist(): + raise FileNotFoundError("manifest.yaml not found in bundle.") - with zf.open(MANIFEST_FILENAME, 'r') as f: + with zf.open("manifest.yaml", 'r') as f: manifest = yaml.safe_load(f) _progress("Updating manifest...") @@ -305,14 +305,14 @@ def edit_bundle_metadata(bundle_path: Path, metadata: dict, progress_callback: O temp_bundle_path = bundle_path.with_suffix('.zip.tmp') with zipfile.ZipFile(temp_bundle_path, 'w', zipfile.ZIP_DEFLATED) as temp_zf: for item in zf.infolist(): - if item.filename == MANIFEST_FILENAME: + if item.filename == "manifest.yaml": continue # Skip old manifest buffer = zf.read(item.filename) temp_zf.writestr(item, buffer) # Write the updated manifest new_manifest_content = yaml.dump(manifest, Dumper=yaml.SafeDumper) - temp_zf.writestr(MANIFEST_FILENAME, new_manifest_content) + temp_zf.writestr("manifest.yaml", new_manifest_content) # Replace the original bundle with the updated one shutil.move(temp_bundle_path, bundle_path)