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.
This commit is contained in:
Kylian Schmidt
2025-07-20 08:59:10 +02:00
parent 2d98ed0e7a
commit dbd67f6039
5 changed files with 347 additions and 29 deletions
+161 -10
View File
@@ -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);
}
+143 -15
View File
@@ -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 = `
<div class="export-instructions">
<h3>Export Instructions</h3>
<p>To export your selected plots, save the following data to a file called <code>export_request.json</code> and run the export script:</p>
<div class="export-data">
<textarea readonly>${requestData}</textarea>
<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-commands">
<p><strong>Command to run:</strong></p>
<code>cd /work/kschmidt/web && python export_plots.py export_request.json</code>
<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-actions">
<button onclick="this.parentElement.parentElement.parentElement.remove()">Close</button>
<button onclick="navigator.clipboard.writeText('${requestData.replace(/'/g, "\\'")}')">Copy JSON</button>
<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>
`;
@@ -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();
});
}
});
+9 -1
View File
@@ -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',
+29 -3
View File
@@ -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()
+5
View File
@@ -108,6 +108,10 @@
<span>Export plots</span>
<span class="shortcut-key">Ctrl+E</span>
</div>
<div class="shortcut-item">
<span>Exit selection mode</span>
<span class="shortcut-key">Esc</span>
</div>
<div class="shortcut-item">
<span>Refresh</span>
<span class="shortcut-key">F5</span>
@@ -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 %}
};
</script>