19fc15abb4
- Implemented a terminal user interface (TUI) for configuring the gallery generator using Textual. - Included functionality to manage sources with add/remove - Integrated a log pane to display generation output from subprocess calls. - Implemented save functionality to persist configuration changes.
445 lines
13 KiB
JavaScript
445 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`;
|
|
|
|
// 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);
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
const tempFilePath = `/tmp/${tempFileName}`;
|
|
const fullCommand = `echo '${requestData.replace(/'/g, "'\\''")}' > ${tempFilePath} && 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>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();
|
|
});
|
|
}
|
|
});
|