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:
+161
-10
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user