From dbd67f60395e49fa7bcf83a84a270d3e23df5f16 Mon Sep 17 00:00:00 2001 From: Kylian Schmidt Date: Sun, 20 Jul 2025 08:59:10 +0200 Subject: [PATCH] Fix template rendering issue and improve export functionality - 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. --- assets/css/export.css | 171 +++++++++++++++++++++++++++++++++--- assets/js/export-manager.js | 158 +++++++++++++++++++++++++++++---- assets/js/utils.js | 10 ++- generate_gallery.py | 32 ++++++- templates/gallery.html | 5 ++ 5 files changed, 347 insertions(+), 29 deletions(-) diff --git a/assets/css/export.css b/assets/css/export.css index 80fcc6e..84d7b5f 100644 --- a/assets/css/export.css +++ b/assets/css/export.css @@ -214,21 +214,37 @@ border: 1px solid var(--border-color); } -.export-commands code { - background: var(--background-color); - padding: 4px 8px; - border-radius: 3px; - font-family: 'Courier New', monospace; +.export-command-container { + margin: 20px 0; border: 1px solid var(--border-color); - display: inline-block; - margin-top: 8px; + border-radius: 8px; + overflow: hidden; +} + +.export-command { + background: var(--header-background); + padding: 16px; + border-bottom: 1px solid var(--border-color); +} + +.export-command code { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 13px; + line-height: 1.4; + color: var(--text-color); + word-break: break-all; + display: block; + background: none; + border: none; + padding: 0; + margin: 0; } .export-actions { display: flex; - gap: 12px; - justify-content: flex-end; - margin-top: 20px; + gap: 8px; + padding: 12px 16px; + background: var(--card-background); } .export-actions button { @@ -254,6 +270,124 @@ .export-actions button:last-child:hover { background: var(--primary-color-dark, #0056b3); } + +.copy-btn, .close-btn { + padding: 8px 16px; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--card-background); + color: var(--text-color); + cursor: pointer; + transition: all 0.2s ease; + font-size: 14px; + display: flex; + align-items: center; + gap: 4px; +} + +.copy-btn:hover { + background: var(--primary-color, #007bff); + color: white; + border-color: var(--primary-color, #007bff); +} + +.close-btn { + background: #dc3545; + color: white; + border-color: #dc3545; + margin-left: auto; +} + +.close-btn:hover { + background: #c82333; + border-color: #bd2130; +} + +.export-details, .export-tips { + margin: 20px 0; + padding: 16px; + border-radius: 6px; + border: 1px solid var(--border-color); +} + +.export-details { + background: var(--header-background); +} + +.export-tips { + background: var(--card-background); + border-color: var(--primary-color, #007bff); + border-left: 4px solid var(--primary-color, #007bff); +} + +.export-details h4, .export-tips h4 { + margin: 0 0 12px 0; + color: var(--text-color); + font-size: 16px; +} + +.export-details ul, .export-tips ul { + margin: 0; + padding-left: 20px; + color: var(--text-color); +} + +.export-details li, .export-tips li { + margin: 8px 0; + line-height: 1.5; +} + +.export-details code, .export-tips code { + background: var(--background-color); + padding: 2px 6px; + border-radius: 3px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 12px; + border: 1px solid var(--border-color); +} + +.export-tips kbd { + background: var(--header-background); + border: 1px solid var(--border-color); + border-radius: 3px; + padding: 2px 6px; + font-family: inherit; + font-size: 12px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.copy-feedback { + position: absolute; + top: 10px; + right: 10px; + padding: 8px 12px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + z-index: 1003; + animation: fadeInOut 2s ease-in-out; +} + +.copy-feedback-success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.copy-feedback-error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +@keyframes fadeInOut { + 0% { opacity: 0; transform: translateY(-10px); } + 20% { opacity: 1; transform: translateY(0); } + 80% { opacity: 1; transform: translateY(0); } + 100% { opacity: 0; transform: translateY(-10px); } +} + +/* Dark theme adjustments */ [data-theme="dark"] .selection-checkbox { background: var(--card-background); border-color: var(--border-color); @@ -287,3 +421,20 @@ color: #f8d7da; border-color: #a94442; } + +[data-theme="dark"] .copy-feedback-success { + background: #155724; + color: #d4edda; + border-color: #1e7e34; +} + +[data-theme="dark"] .copy-feedback-error { + background: #721c24; + color: #f8d7da; + border-color: #a94442; +} + +[data-theme="dark"] .export-tips kbd { + background: var(--background-color); + color: var(--text-color); +} diff --git a/assets/js/export-manager.js b/assets/js/export-manager.js index ad9daeb..b60967b 100644 --- a/assets/js/export-manager.js +++ b/assets/js/export-manager.js @@ -52,7 +52,7 @@ export class ExportManager { this.toggleSelectionMode(); } if (e.key === 'Escape') { - this.clearSelection(); + this.exitSelectionMode(); } }); @@ -118,10 +118,17 @@ export class ExportManager { * 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'); + } } /** @@ -229,17 +236,26 @@ export class ExportManager { 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: `merged_plots_${new Date().toISOString().split('T')[0]}.pdf` + 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); - // Since we can't directly call Python from the browser, we'll show instructions - this.showExportInstructions(requestData); + // Show improved export instructions with full command + this.showExportInstructions(requestData, tempFileName, workDir); } /** @@ -258,21 +274,49 @@ export class ExportManager { /** * Show export instructions to user */ - showExportInstructions(requestData) { + 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 Instructions

-

To export your selected plots, save the following data to a file called export_request.json and run the export script:

-
- +

🚀 Export Selected Plots

+

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

+ +
+
+ ${fullCommand} +
+
+ + + +
-
-

Command to run:

- cd /work/kschmidt/web && python export_plots.py export_request.json + +
+

📋 Command Breakdown:

+
    +
  • Creates temporary file: ${tempFilePath}
  • +
  • Changes to work directory: ${workDir}
  • +
  • Runs export script: python export_plots.py
  • +
  • Output file: Will be saved in the work directory
  • +
-
- - + +
+

💡 Tips:

+
    +
  • The temporary JSON file will be automatically cleaned up after successful export
  • +
  • Use Esc to exit selection mode
  • +
  • Press Ctrl+E to toggle selection mode
  • +
`; @@ -280,7 +324,59 @@ export class ExportManager { 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); + } + }); } /** @@ -318,3 +414,35 @@ export class ExportManager { }, 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(); + }); + } +}); diff --git a/assets/js/utils.js b/assets/js/utils.js index 78fb7e5..f9817d4 100644 --- a/assets/js/utils.js +++ b/assets/js/utils.js @@ -105,8 +105,16 @@ export class Utils { refreshBtn.disabled = true; refreshBtn.textContent = '⏳'; + // Use the CGI script path from config if available + let cgiPath = '/cgi-bin/refresh_gallery.py'; + if (window.galleryConfig && window.galleryConfig.paths && window.galleryConfig.paths.cgi_script) { + cgiPath = window.galleryConfig.paths.cgi_script; + // Ensure it starts with a slash for fetch + if (!cgiPath.startsWith('/')) cgiPath = '/' + cgiPath; + } + try { - const response = await fetch('/cgi-bin/refresh_gallery.py', { + const response = await fetch(cgiPath, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/generate_gallery.py b/generate_gallery.py index e0ff44b..e878a78 100644 --- a/generate_gallery.py +++ b/generate_gallery.py @@ -15,6 +15,8 @@ Features: import subprocess import shutil +import os +import sys from pathlib import Path from typing import Dict, Any, Optional from jinja2 import Environment, FileSystemLoader @@ -114,6 +116,7 @@ def build_gallery(source_dir: Path, web_dir: Path, current_metadata = merge_metadata(inherited_metadata, folder_metadata) pdf_files = list(source_dir.glob("*.pdf")) + subdirs = [d for d in source_dir.iterdir() if d.is_dir()] items = [] @@ -188,16 +191,18 @@ def build_gallery(source_dir: Path, web_dir: Path, assets_path = "../" * (depth + 1) + "assets" with output_html.open("w") as f: - f.write(template.render( + rendered_html = template.render( title=title, items=items, subdirs=subdir_names, relpath=str(relative_path), + paths=CONFIG.paths, ui=CONFIG.ui, stats=stats, folder_metadata=current_metadata, assets_path=assets_path - )) + ) + f.write(rendered_html) print(f"Generated {output_html}") @@ -262,6 +267,21 @@ def format_file_size(size_bytes: int) -> str: return f"{size:.1f} {size_names[i]}" +def refresh_gallery_cgi(): + """ + CGI handler to refresh the gallery from a web request. + Outputs a minimal HTTP response and triggers gallery regeneration. + """ + import traceback + print("Content-Type: text/plain\n") + try: + main() + print("Gallery refreshed successfully.") + except Exception as e: + print(f"Error refreshing gallery: {e}") + traceback.print_exc(file=sys.stdout) + + def main() -> None: """ Main entry point for gallery generation. @@ -356,6 +376,7 @@ def main() -> None: items=items, subdirs=[], relpath=source.name, + paths=CONFIG.paths, ui=CONFIG.ui, stats=stats, folder_metadata={}, @@ -375,7 +396,12 @@ def main() -> None: f"directory nor a PDF file") print("Done") + # No copying of this script to CGI location if __name__ == "__main__": - main() + # If run as CGI, call the CGI handler + if 'GATEWAY_INTERFACE' in os.environ: + refresh_gallery_cgi() + else: + main() diff --git a/templates/gallery.html b/templates/gallery.html index e345b2c..6de9970 100644 --- a/templates/gallery.html +++ b/templates/gallery.html @@ -108,6 +108,10 @@ Export plots Ctrl+E
+
+ Exit selection mode + Esc +
Refresh F5 @@ -186,6 +190,7 @@ window.galleryConfig = { searchDebounceMs: {{ ui.search_debounce_ms|default(300) }}, maxRecentPlots: {{ ui.max_recent_plots|default(20) }}, + workDir: "{{ paths.work_dir }}", stats: {% if stats %}{{ stats|tojson }}{% else %}null{% endif %} };