dbd67f6039
- Fix template rendering by storing rendered HTML in variable before writing This resolves subdirectories not appearing in navigation despite being correctly detected and included in template variables - Add missing template variables (paths.work_dir) to gallery config - Add CGI refresh handler for web-based gallery regeneration - Improve PDF export functionality with better error handling - Add ESC key shortcut documentation for exiting selection mode - Enhanced export manager with proper dependency checking Fixes issue where new subdirectories were detected but not displayed in browser navigation due to incomplete template rendering.
449 lines
13 KiB
JavaScript
449 lines
13 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<div class="selection-checkbox">
|
|
<span class="checkbox-icon">☐</span>
|
|
</div>
|
|
`;
|
|
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 = `
|
|
<div class="export-instructions">
|
|
<h3>🚀 Export Selected Plots</h3>
|
|
<p>Run the following command in your terminal to export the selected plots:</p>
|
|
|
|
<div class="export-command-container">
|
|
<div class="export-command">
|
|
<code id="exportCommand">${fullCommand}</code>
|
|
</div>
|
|
<div class="export-actions">
|
|
<button onclick="this.copyCommand()" class="copy-btn" title="Copy command to clipboard">
|
|
📋 Copy Command
|
|
</button>
|
|
<button onclick="this.copyJSON()" class="copy-btn" title="Copy JSON only">
|
|
📄 Copy JSON
|
|
</button>
|
|
<button onclick="this.close()" class="close-btn">
|
|
✕ Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="export-details">
|
|
<h4>📋 Command Breakdown:</h4>
|
|
<ul>
|
|
<li><strong>Creates temporary file:</strong> <code>${tempFilePath}</code></li>
|
|
<li><strong>Changes to work directory:</strong> <code>${workDir}</code></li>
|
|
<li><strong>Runs export script:</strong> <code>python export_plots.py</code></li>
|
|
<li><strong>Output file:</strong> Will be saved in the work directory</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="export-tips">
|
|
<h4>💡 Tips:</h4>
|
|
<ul>
|
|
<li>The temporary JSON file will be automatically cleaned up after successful export</li>
|
|
<li>Use <kbd>Esc</kbd> to exit selection mode</li>
|
|
<li>Press <kbd>Ctrl+E</kbd> to toggle selection mode</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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();
|
|
});
|
|
}
|
|
});
|