/** * Export Manager for Gallery * Handles exporting selected plots to merged PDF */ export class ExportManager { constructor() { this.selectedPlots = new Set(); this.maxPlots = 4; this.init(); } init() { this.createExportButton(); this.bindEvents(); } /** * Create the export button in the floating buttons section */ createExportButton() { const floatingButtons = document.querySelector('.floating-buttons'); if (!floatingButtons) return; const exportBtn = document.createElement('button'); exportBtn.className = 'floating-btn export-btn'; exportBtn.id = 'exportBtn'; exportBtn.title = 'Export Selected Plots (Ctrl+E)'; exportBtn.innerHTML = '📄'; exportBtn.style.display = 'none'; // Hidden by default exportBtn.onclick = () => this.exportSelectedPlots(); floatingButtons.appendChild(exportBtn); // Add selection counter const selectionCounter = document.createElement('div'); selectionCounter.className = 'selection-counter'; selectionCounter.id = 'selectionCounter'; selectionCounter.style.display = 'none'; selectionCounter.innerHTML = '0/4 selected'; floatingButtons.appendChild(selectionCounter); } /** * Bind events for plot selection */ bindEvents() { // Add selection mode toggle document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key === 'e') { e.preventDefault(); this.toggleSelectionMode(); } if (e.key === 'Escape') { this.exitSelectionMode(); } }); // Add selection handlers to existing plots this.addSelectionHandlers(); } /** * Add selection handlers to all plot items */ addSelectionHandlers() { const plotItems = document.querySelectorAll('.grid-item'); plotItems.forEach(item => this.addSelectionHandler(item)); } /** * Add selection handler to a single plot item */ addSelectionHandler(item) { // Create selection overlay const overlay = document.createElement('div'); overlay.className = 'selection-overlay'; overlay.innerHTML = `
`; item.appendChild(overlay); // Add click handler for selection overlay.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); this.togglePlotSelection(item); }); } /** * Toggle selection mode */ toggleSelectionMode() { const body = document.body; const isSelectionMode = body.classList.contains('selection-mode'); if (isSelectionMode) { this.exitSelectionMode(); } else { this.enterSelectionMode(); } } /** * Enter selection mode */ enterSelectionMode() { document.body.classList.add('selection-mode'); document.getElementById('exportBtn').style.display = 'block'; document.getElementById('selectionCounter').style.display = 'block'; this.updateSelectionCounter(); } /** * Exit selection mode */ exitSelectionMode() { const wasInSelectionMode = document.body.classList.contains('selection-mode'); document.body.classList.remove('selection-mode'); document.getElementById('exportBtn').style.display = 'none'; document.getElementById('selectionCounter').style.display = 'none'; this.clearSelection(); // Show message if user was actually in selection mode if (wasInSelectionMode) { this.showMessage('Exited selection mode', 'info'); } } /** * Toggle plot selection */ togglePlotSelection(item) { const plotName = this.getPlotName(item); const plotPath = this.getPlotPath(item); if (this.selectedPlots.has(plotName)) { this.selectedPlots.delete(plotName); item.classList.remove('selected'); item.querySelector('.checkbox-icon').textContent = '☐'; } else { if (this.selectedPlots.size >= this.maxPlots) { this.showMessage(`Maximum ${this.maxPlots} plots can be selected`, 'warning'); return; } this.selectedPlots.add(plotName); item.classList.add('selected'); item.querySelector('.checkbox-icon').textContent = '☑'; } this.updateSelectionCounter(); } /** * Get plot name from grid item */ getPlotName(item) { const plotName = item.querySelector('.plot-name'); return plotName ? plotName.textContent.trim() : ''; } /** * Get plot PDF path from grid item */ getPlotPath(item) { const link = item.querySelector('a[href$=".pdf"]'); return link ? link.href : ''; } /** * Update selection counter */ updateSelectionCounter() { const counter = document.getElementById('selectionCounter'); if (counter) { counter.textContent = `${this.selectedPlots.size}/${this.maxPlots} selected`; } const exportBtn = document.getElementById('exportBtn'); if (exportBtn) { exportBtn.disabled = this.selectedPlots.size === 0; exportBtn.style.opacity = this.selectedPlots.size === 0 ? '0.5' : '1'; } } /** * Clear all selections */ clearSelection() { this.selectedPlots.clear(); document.querySelectorAll('.grid-item.selected').forEach(item => { item.classList.remove('selected'); const checkbox = item.querySelector('.checkbox-icon'); if (checkbox) checkbox.textContent = '☐'; }); this.updateSelectionCounter(); } /** * Export selected plots to merged PDF */ async exportSelectedPlots() { if (this.selectedPlots.size === 0) { this.showMessage('No plots selected', 'warning'); return; } const plotPaths = Array.from(this.selectedPlots).map(plotName => { const item = Array.from(document.querySelectorAll('.grid-item')) .find(item => this.getPlotName(item) === plotName); return this.getPlotPath(item); }); this.showMessage('Preparing export...', 'info'); try { await this.createMergedPDF(plotPaths); } catch (error) { this.showMessage('Export failed: ' + error.message, 'error'); } } /** * Create merged PDF using Python script */ async createMergedPDF(plotPaths) { // Convert file:// URLs to actual paths const actualPaths = plotPaths.map(url => { if (url.startsWith('file://')) { return url.substring(7); // Remove 'file://' prefix } return url; }); const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0]; const outputName = `merged_plots_${timestamp}.pdf`; const payload = { plots: actualPaths, layout: this.calculateLayout(actualPaths.length), output_name: outputName }; // Generate a unique temporary filename const tempFileName = `export_request_${Date.now()}.json`; // Get work directory from config or fallback const workDir = window.galleryConfig?.workDir || '/work/kschmidt/web'; // Save the request to a JSON file that can be picked up by a Python script const requestData = JSON.stringify(payload, null, 2); // Show improved export instructions with full command this.showExportInstructions(requestData, tempFileName, workDir); } /** * Calculate optimal layout for given number of plots */ calculateLayout(numPlots) { switch (numPlots) { case 1: return { rows: 1, cols: 1 }; case 2: return { rows: 1, cols: 2 }; case 3: return { rows: 2, cols: 2 }; // 3 plots in 2x2 grid with one empty case 4: return { rows: 2, cols: 2 }; default: return { rows: 2, cols: 2 }; } } /** * Show export instructions to user */ showExportInstructions(requestData, tempFileName, workDir) { const tempFilePath = `/tmp/${tempFileName}`; const fullCommand = `echo '${requestData.replace(/'/g, "'\\''")}' > ${tempFilePath} && cd ${workDir} && python export_plots.py ${tempFilePath}`; const instructions = `

🚀 Export Selected Plots

Run the following command in your terminal to export the selected plots:

${fullCommand}

📋 Command Breakdown:

💡 Tips:

`; const overlay = document.createElement('div'); overlay.className = 'export-overlay'; overlay.innerHTML = instructions; // Add methods to the overlay for button handlers overlay.copyCommand = function() { navigator.clipboard.writeText(fullCommand).then(() => { this.showCopyFeedback('Command copied to clipboard!'); }).catch(() => { this.showCopyFeedback('Failed to copy. Please select and copy manually.', 'error'); }); }; overlay.copyJSON = function() { navigator.clipboard.writeText(requestData).then(() => { this.showCopyFeedback('JSON copied to clipboard!'); }).catch(() => { this.showCopyFeedback('Failed to copy. Please select and copy manually.', 'error'); }); }; overlay.close = function() { this.remove(); }; overlay.showCopyFeedback = function(message, type = 'success') { const feedback = document.createElement('div'); feedback.className = `copy-feedback copy-feedback-${type}`; feedback.textContent = message; this.appendChild(feedback); setTimeout(() => { if (feedback.parentNode) { feedback.parentNode.removeChild(feedback); } }, 2000); }; document.body.appendChild(overlay); // Close on ESC key const handleEscape = (e) => { if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', handleEscape); } }; document.addEventListener('keydown', handleEscape); // Close on clicking outside overlay.addEventListener('click', (e) => { if (e.target === overlay) { overlay.remove(); document.removeEventListener('keydown', handleEscape); } }); } /** * Download the PDF blob */ downloadPDF(blob) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `merged_plots_${new Date().toISOString().split('T')[0]}.pdf`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } /** * Show temporary message to user */ showMessage(text, type = 'info') { // Remove existing message const existing = document.querySelector('.export-message'); if (existing) existing.remove(); const message = document.createElement('div'); message.className = `export-message export-message-${type}`; message.textContent = text; document.body.appendChild(message); setTimeout(() => { if (message.parentNode) { message.parentNode.removeChild(message); } }, 3000); } } // Add these methods to ExportManager if not present ExportManager.prototype.isSelectionModeActive = function() { return document.body.classList.contains('selection-mode'); }; ExportManager.prototype.exitSelectionMode = function() { document.body.classList.remove('selection-mode'); if (typeof this.clearSelection === 'function') { this.clearSelection(); } }; // Ensure a single global instance window.exportManager = window.exportManager || new ExportManager(); // Listen for ESC key globally to exit selection mode // (This will work even if focus is not on a plot) document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && window.exportManager && window.exportManager.isSelectionModeActive()) { window.exportManager.exitSelectionMode(); } }); // Attach improved export logic to export button document.addEventListener('DOMContentLoaded', function() { const exportBtn = document.getElementById('exportBtn'); if (exportBtn) { exportBtn.addEventListener('click', function() { window.exportManager.exportSelectedPlots(); }); } });