559 lines
19 KiB
JavaScript
559 lines
19 KiB
JavaScript
// 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, keyOperations, pluginOperations, fillWorkflow, uiComponents, opatHandler;
|
|
|
|
// --- EVENT LISTENERS SETUP ---
|
|
function setupEventListeners() {
|
|
const elements = domManager.getElements();
|
|
|
|
// Theme updates
|
|
ipcRenderer.on('theme-updated', (event, { shouldUseDarkColors }) => {
|
|
document.body.classList.toggle('dark-mode', shouldUseDarkColors);
|
|
});
|
|
|
|
// File association handlers
|
|
ipcRenderer.on('open-bundle-file', async (event, filePath) => {
|
|
console.log(`[RENDERER] Opening .fbundle file via association: ${filePath}`);
|
|
try {
|
|
// Switch to libplugin category if not already there
|
|
const libpluginCategory = document.querySelector('.category-item[data-category="libplugin"]');
|
|
if (libpluginCategory && !libpluginCategory.classList.contains('active')) {
|
|
libpluginCategory.click();
|
|
}
|
|
|
|
// Open the bundle
|
|
await bundleOperations.openBundleFromPath(filePath);
|
|
} catch (error) {
|
|
console.error('[RENDERER] Error opening bundle file:', error);
|
|
domManager.showModal('File Open Error', `Failed to open bundle file: ${error.message}`);
|
|
}
|
|
});
|
|
|
|
ipcRenderer.on('open-opat-file', async (event, filePath) => {
|
|
console.log(`[RENDERER] Opening .opat file via association: ${filePath}`);
|
|
try {
|
|
// Switch to OPAT Core category
|
|
const opatCategory = document.querySelector('.category-item[data-category="opat"]');
|
|
if (opatCategory) {
|
|
opatCategory.click();
|
|
}
|
|
|
|
// Wait a moment for category switching to complete
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
// Open the OPAT file using the OPAT handler
|
|
if (opatHandler && opatHandler.openOpatFromPath) {
|
|
await opatHandler.openOpatFromPath(filePath);
|
|
} else {
|
|
console.warn('[RENDERER] OPAT file opening not available');
|
|
domManager.showModal('Error', 'OPAT file opening functionality is not available.');
|
|
}
|
|
} catch (error) {
|
|
console.error('[RENDERER] Error opening OPAT file:', error);
|
|
domManager.showModal('File Open Error', `Failed to open OPAT file: ${error.message}`);
|
|
}
|
|
});
|
|
|
|
// 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.');
|
|
});
|
|
|
|
// Plugin management navigation is handled by plugin-operations.js
|
|
// No duplicate event listeners needed here
|
|
|
|
// 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));
|
|
|
|
// Key Management event listeners
|
|
setupKeyManagementEventListeners();
|
|
|
|
// Plugin Management event listeners
|
|
console.log('[EVENT_HANDLERS] Setting up plugin event listeners...');
|
|
console.log('[EVENT_HANDLERS] pluginOperations available:', !!pluginOperations);
|
|
console.log('[EVENT_HANDLERS] setupPluginEventListeners method available:', !!(pluginOperations && pluginOperations.setupPluginEventListeners));
|
|
|
|
if (pluginOperations && pluginOperations.setupPluginEventListeners) {
|
|
pluginOperations.setupPluginEventListeners();
|
|
} else {
|
|
console.warn('[EVENT_HANDLERS] Plugin operations not available for event listener setup');
|
|
}
|
|
|
|
// 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);
|
|
|
|
// Set up plugin button listeners when libplugin category is shown
|
|
if (category === 'libplugin' && pluginOperations) {
|
|
// Use setTimeout to ensure DOM is ready
|
|
setTimeout(() => {
|
|
pluginOperations.ensurePluginButtonListeners();
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
// 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',
|
|
'docs': 'Welcome to Documentation'
|
|
};
|
|
|
|
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...',
|
|
'docs': 'Browse and view Doxygen documentation for 4DSTAR libraries.'
|
|
};
|
|
|
|
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', 'docs-home', 'opat-view', 'libplugin-view',
|
|
'bundle-view', 'keys-view', 'create-bundle-form', 'plugin-view', 'docs-viewer'
|
|
];
|
|
|
|
// 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',
|
|
'docs': 'docs-home',
|
|
'keys': 'keys-view'
|
|
};
|
|
|
|
const viewId = viewMap[category] || 'welcome-screen';
|
|
const view = document.getElementById(viewId);
|
|
if (view) {
|
|
view.classList.remove('hidden');
|
|
|
|
// Initialize key management when showing keys view
|
|
if (category === 'keys' && keyOperations) {
|
|
// Show the default keys list view and load keys
|
|
showKeyManagementView('keys-list-view');
|
|
keyOperations.loadTrustedKeys();
|
|
|
|
// Set the keys-list-btn as active in sidebar
|
|
const keysSidebarButtons = document.querySelectorAll('#keys-list-btn, #keys-generate-btn, #keys-add-btn, #keys-remotes-btn');
|
|
keysSidebarButtons.forEach(btn => btn.classList.remove('active'));
|
|
const keysListBtn = document.getElementById('keys-list-btn');
|
|
if (keysListBtn) keysListBtn.classList.add('active');
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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', async (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');
|
|
|
|
// Load license content when license tab is clicked
|
|
if (targetTab === 'license-info-tab') {
|
|
console.log('[FRONTEND] License tab clicked, loading content...');
|
|
await loadLicenseContent();
|
|
} else {
|
|
console.log(`[FRONTEND] Tab clicked: ${targetTab}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
// External link handling
|
|
const githubLink = document.getElementById('github-link');
|
|
if (githubLink) {
|
|
githubLink.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
// Get the URL from the href attribute instead of hardcoding
|
|
const url = githubLink.getAttribute('href');
|
|
ipcRenderer.invoke('open-external-url', url);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Hide info modal - make it globally accessible
|
|
function hideInfoModal() {
|
|
const infoModal = document.getElementById('info-modal');
|
|
if (infoModal) infoModal.classList.add('hidden');
|
|
}
|
|
|
|
// Load license content from LICENSE.txt file
|
|
async function loadLicenseContent() {
|
|
console.log('[FRONTEND] loadLicenseContent() called');
|
|
const licenseTextarea = document.querySelector('.license-text');
|
|
console.log('[FRONTEND] License textarea found:', licenseTextarea);
|
|
if (!licenseTextarea) {
|
|
console.error('[FRONTEND] License textarea not found!');
|
|
return;
|
|
}
|
|
|
|
// Don't reload if already loaded (not placeholder)
|
|
if (licenseTextarea.value && !licenseTextarea.value.includes('GPL v3 license text will be pasted here')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log('[FRONTEND] Requesting license content...');
|
|
const result = await ipcRenderer.invoke('read-license');
|
|
console.log('[FRONTEND] License result:', result);
|
|
|
|
if (result.success) {
|
|
console.log(`[FRONTEND] License content length: ${result.content.length} characters`);
|
|
console.log(`[FRONTEND] License starts with: "${result.content.substring(0, 100)}..."`);
|
|
console.log(`[FRONTEND] License ends with: "...${result.content.substring(result.content.length - 100)}"`);
|
|
|
|
licenseTextarea.value = result.content;
|
|
licenseTextarea.placeholder = '';
|
|
// Scroll to the top to show the beginning of the license
|
|
licenseTextarea.scrollTop = 0;
|
|
} else {
|
|
licenseTextarea.value = result.content; // Fallback error message
|
|
licenseTextarea.placeholder = '';
|
|
console.error('Failed to load license:', result.error);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading license content:', error);
|
|
licenseTextarea.value = 'Error loading license content. Please check that LICENSE.txt exists in the application directory.';
|
|
licenseTextarea.placeholder = '';
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
// Setup key management event listeners
|
|
function setupKeyManagementEventListeners() {
|
|
// Key management sidebar navigation
|
|
const keysSidebarButtons = document.querySelectorAll('#keys-list-btn, #keys-generate-btn, #keys-add-btn, #keys-remotes-btn');
|
|
keysSidebarButtons.forEach(button => {
|
|
button.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
|
|
// Update active sidebar button
|
|
keysSidebarButtons.forEach(btn => btn.classList.remove('active'));
|
|
button.classList.add('active');
|
|
|
|
// Show appropriate key management view
|
|
const viewMap = {
|
|
'keys-list-btn': 'keys-list-view',
|
|
'keys-generate-btn': 'keys-generate-view',
|
|
'keys-add-btn': 'keys-add-view',
|
|
'keys-remotes-btn': 'keys-remotes-view'
|
|
};
|
|
|
|
const targetView = viewMap[button.id];
|
|
if (targetView) {
|
|
showKeyManagementView(targetView);
|
|
|
|
// Load content for specific views
|
|
if (targetView === 'keys-list-view') {
|
|
keyOperations.loadTrustedKeys();
|
|
} else if (targetView === 'keys-remotes-view') {
|
|
keyOperations.loadRemoteSources();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Generate key button
|
|
const generateKeyBtn = document.getElementById('generate-key-btn');
|
|
if (generateKeyBtn) {
|
|
generateKeyBtn.addEventListener('click', keyOperations.handleGenerateKey);
|
|
}
|
|
|
|
// Add remote button
|
|
const addRemoteBtn = document.getElementById('add-remote-btn');
|
|
if (addRemoteBtn) {
|
|
addRemoteBtn.addEventListener('click', keyOperations.handleAddRemote);
|
|
}
|
|
|
|
// Add key file button
|
|
const addKeyFileBtn = document.getElementById('add-key-file-btn');
|
|
if (addKeyFileBtn) {
|
|
addKeyFileBtn.addEventListener('click', keyOperations.handleAddKey);
|
|
}
|
|
|
|
// Dynamic buttons (will be added dynamically to keys list)
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('sync-remotes-btn')) {
|
|
keyOperations.handleSyncRemotes();
|
|
} else if (e.target.classList.contains('remove-remote-btn')) {
|
|
const remoteName = e.target.dataset.remoteName;
|
|
keyOperations.handleRemoveRemote(remoteName);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Show specific key management view
|
|
function showKeyManagementView(viewId) {
|
|
// Hide all key management views
|
|
const keyViews = document.querySelectorAll('.key-management-view');
|
|
keyViews.forEach(view => view.classList.add('hidden'));
|
|
|
|
// Show the target view
|
|
const targetView = document.getElementById(viewId);
|
|
if (targetView) {
|
|
targetView.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
// Initialize dependencies (called when module is loaded)
|
|
function initializeDependencies(deps) {
|
|
stateManager = deps.stateManager;
|
|
domManager = deps.domManager;
|
|
bundleOperations = deps.bundleOperations;
|
|
keyOperations = deps.keyOperations;
|
|
pluginOperations = deps.pluginOperations;
|
|
fillWorkflow = deps.fillWorkflow;
|
|
uiComponents = deps.uiComponents;
|
|
opatHandler = deps.opatHandler;
|
|
}
|
|
|
|
module.exports = {
|
|
initializeDependencies,
|
|
setupEventListeners,
|
|
setupKeyManagementEventListeners,
|
|
showKeyManagementView,
|
|
checkSignatureAndWarn,
|
|
setupCategoryNavigation,
|
|
updateWelcomeScreen,
|
|
showCategoryHomeScreen,
|
|
setupInfoModal,
|
|
hideInfoModal,
|
|
handleSaveMetadata
|
|
};
|