From f3adbe49fac8ba7648453e6c5485f0f62248c39d Mon Sep 17 00:00:00 2001 From: Kylian Schmidt Date: Tue, 29 Jul 2025 09:38:28 +0200 Subject: [PATCH] Added so much stuff I cant keep up. I think some sorting capability and a way to add metadata to each folder (WIP) --- .vscode/settings.json | 6 + assets/css/floating-elements.css | 15 -- assets/css/folder-metadata.css | 203 ++++++++++++++++++ assets/css/main.css | 4 + assets/css/responsive.css | 11 + assets/css/sort-controls.css | 88 ++++++++ .../css/sort-debug.css | 0 assets/css/view-controls.css | 101 ++++++++- assets/css/view-override.css | 4 +- assets/js/folder-metadata.js | 69 ++++++ assets/js/gallery-app.js | 4 +- assets/js/keyboard-manager.js | 20 +- assets/js/main.js | 2 - assets/js/sort-manager.js | 197 +++++++++++++++++ assets/js/utils.js | 44 ---- assets/js/view-manager.js | 19 +- debug_template.py | 49 ----- docs/EXPORT_FUNCTIONALITY.md | 101 --------- docs/METADATA_IMPLEMENTATION.md | 103 --------- docs/METADATA_USAGE.md | 114 ---------- docs/SORT_IMPLEMENTATION.md | 136 ++++++++++++ examples/metadata.json | 19 ++ export_api.py | 94 -------- export_plots.py | 107 --------- generate_gallery.py | 26 ++- orchestration/metadata.py | 6 +- python/add_creation_time.py | 92 ++++++++ python/add_metadata.py | 45 ++++ templates/gallery.html | 152 +++++++++---- test_metadata.html | 0 30 files changed, 1134 insertions(+), 697 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 assets/css/folder-metadata.css create mode 100644 assets/css/sort-controls.css rename refresh_gallery.py => assets/css/sort-debug.css (100%) mode change 100755 => 100644 create mode 100644 assets/js/folder-metadata.js create mode 100644 assets/js/sort-manager.js delete mode 100644 debug_template.py delete mode 100644 docs/EXPORT_FUNCTIONALITY.md delete mode 100644 docs/METADATA_IMPLEMENTATION.md delete mode 100644 docs/METADATA_USAGE.md create mode 100644 docs/SORT_IMPLEMENTATION.md create mode 100644 examples/metadata.json delete mode 100644 export_api.py delete mode 100644 export_plots.py create mode 100644 python/add_creation_time.py create mode 100644 python/add_metadata.py delete mode 100644 test_metadata.html diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..47b059e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "flake8.args": [ + "--max-line-length=120", + "--ignore=W293,E123,W503", + ], +} \ No newline at end of file diff --git a/assets/css/floating-elements.css b/assets/css/floating-elements.css index 1c5bb2f..ae6a70a 100644 --- a/assets/css/floating-elements.css +++ b/assets/css/floating-elements.css @@ -39,21 +39,6 @@ color: white; } -.refresh-btn { - background: var(--success-color); - color: white; -} - -.refresh-btn:hover { - background: var(--success-hover); -} - -.refresh-btn:disabled { - background: var(--disabled-color); - cursor: not-allowed; - transform: none; -} - /* ======================================== KEYBOARD SHORTCUTS HELP ======================================== */ diff --git a/assets/css/folder-metadata.css b/assets/css/folder-metadata.css new file mode 100644 index 0000000..b41a242 --- /dev/null +++ b/assets/css/folder-metadata.css @@ -0,0 +1,203 @@ +/* ======================================== + FOLDER METADATA DISPLAY STYLES + ======================================== */ + +.folder-metadata-container { + margin: 2rem 0; + padding: 1.5rem; + background: var(--card-background); + border: 1px solid var(--border-color); + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.folder-metadata-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 2px solid var(--border-color); +} + +.folder-metadata-title { + margin: 0; + color: var(--text-color); + font-size: 1.25rem; + font-weight: 600; +} + +.folder-metadata-edit-btn { + background: var(--accent-color); + color: white; + border: none; + border-radius: 6px; + padding: 0.5rem 1rem; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.folder-metadata-edit-btn:hover { + background: var(--accent-color-dark); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.folder-metadata-edit-btn:active { + transform: translateY(0); +} + +.folder-metadata-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1rem; +} + +.folder-metadata-item { + display: flex; + flex-direction: column; + background: var(--background); + border: 1px solid var(--border-color-light); + border-radius: 6px; + padding: 1rem; + transition: all 0.2s ease; +} + +.folder-metadata-item:hover { + background: var(--hover-background); + border-color: var(--accent-color); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.folder-metadata-key { + font-weight: 600; + color: var(--accent-color); + margin-bottom: 0.5rem; + font-size: 0.95rem; + text-transform: capitalize; + border-bottom: 1px solid var(--border-color-light); + padding-bottom: 0.25rem; +} + +.folder-metadata-value { + color: var(--text-color); + line-height: 1.5; + word-break: break-word; +} + +.folder-metadata-value a { + color: var(--accent-color); + text-decoration: none; + border-bottom: 1px dotted var(--accent-color); + transition: all 0.2s ease; +} + +.folder-metadata-value a:hover { + color: var(--accent-color-dark); + border-bottom-style: solid; +} + +.folder-metadata-list { + margin: 0; + padding-left: 1.5rem; +} + +.folder-metadata-list li { + margin-bottom: 0.25rem; +} + +.folder-metadata-nested { + background: var(--background-dark); + padding: 0.75rem; + border-radius: 4px; + border-left: 3px solid var(--accent-color); +} + +.folder-metadata-nested-item { + margin-bottom: 0.5rem; +} + +.folder-metadata-nested-item:last-child { + margin-bottom: 0; +} + +.folder-metadata-nested-item strong { + color: var(--accent-color); +} + +.folder-metadata-long-text { + display: inline; +} + +.folder-metadata-expand { + background: none; + border: none; + color: var(--accent-color); + cursor: pointer; + text-decoration: underline; + font-size: 0.9rem; + margin-left: 0.5rem; + padding: 0; +} + +.folder-metadata-expand:hover { + color: var(--accent-color-dark); +} + +.folder-metadata-full-text { + display: none; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .folder-metadata-container { + margin: 1rem 0; + padding: 1rem; + } + + .folder-metadata-grid { + grid-template-columns: 1fr; + gap: 0.75rem; + } + + .folder-metadata-header { + flex-direction: column; + align-items: flex-start; + gap: 0.75rem; + } + + .folder-metadata-edit-btn { + align-self: flex-end; + } +} + +/* Dark theme adjustments */ +@media (prefers-color-scheme: dark) { + .folder-metadata-container { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + } + + .folder-metadata-item:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + } +} + +/* Animation for expanding text */ +.folder-metadata-full-text.show { + display: inline; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/assets/css/main.css b/assets/css/main.css index 771752c..3713837 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -16,11 +16,15 @@ @import url('./stats.css'); @import url('./comparison.css'); @import url('./metadata.css'); +@import url('./folder-metadata.css'); @import url('./export.css'); /* View controls - must come after grid.css to override */ @import url('./view-controls.css'); +/* Sort controls styling */ +@import url('./sort-controls.css'); + /* View override - force grid layout to work */ @import url('./view-override.css'); diff --git a/assets/css/responsive.css b/assets/css/responsive.css index 04f7bda..1ca81f5 100644 --- a/assets/css/responsive.css +++ b/assets/css/responsive.css @@ -30,9 +30,20 @@ } /* View controls responsive */ + .controls-container { + flex-direction: column; + gap: 1rem; + align-items: stretch; + } + + .sort-controls { + justify-content: center; + } + .view-controls { margin-right: 0.5rem !important; padding: 8px 12px !important; + justify-content: center; } .view-btn { diff --git a/assets/css/sort-controls.css b/assets/css/sort-controls.css new file mode 100644 index 0000000..3df2439 --- /dev/null +++ b/assets/css/sort-controls.css @@ -0,0 +1,88 @@ +/* ======================================== + SORT CONTROLS STYLING + ======================================== */ + +/* Clean styling for sort controls */ +.sort-controls { + display: flex !important; + align-items: center !important; + gap: 8px !important; + padding: 12px 16px !important; + background: var(--tree-bg) !important; + border: 1px solid var(--border-color) !important; + border-radius: 8px !important; + margin-right: 1rem !important; +} + +.sort-label { + font-size: 0.9rem !important; + color: var(--text-color) !important; + margin-right: 8px !important; + font-weight: 500 !important; +} + +/* Button styling using theme variables */ +.sort-btn { + background: var(--card-bg) !important; + border: 1px solid var(--border-color) !important; + border-radius: 6px !important; + padding: 8px 12px !important; + cursor: pointer !important; + transition: all 0.2s ease !important; + font-size: 0.85rem !important; + color: var(--text-color) !important; + display: flex !important; + align-items: center !important; + gap: 4px !important; + outline: none !important; + text-decoration: none !important; + font-family: inherit !important; +} + +.sort-btn:hover { + background: var(--button-bg) !important; + color: white !important; + transform: translateY(-1px) !important; + box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; + border-color: var(--button-bg) !important; +} + +.sort-btn.active { + background: var(--button-bg) !important; + color: white !important; + border-color: var(--button-bg) !important; + box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; +} + +.sort-order-btn { + background: var(--card-bg) !important; + border: 1px solid var(--border-color) !important; + border-radius: 6px !important; + padding: 8px 12px !important; + cursor: pointer !important; + transition: all 0.2s ease !important; + font-size: 1rem !important; + color: var(--text-color) !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + min-width: 36px !important; + outline: none !important; + text-decoration: none !important; + font-family: inherit !important; +} + +.sort-order-btn:hover { + background: var(--button-bg) !important; + color: white !important; + transform: translateY(-1px) !important; + box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; + border-color: var(--button-bg) !important; +} + +.sort-order-btn.active { + background: var(--button-bg) !important; + color: white !important; + border-color: var(--button-bg) !important; + box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; +} diff --git a/refresh_gallery.py b/assets/css/sort-debug.css old mode 100755 new mode 100644 similarity index 100% rename from refresh_gallery.py rename to assets/css/sort-debug.css diff --git a/assets/css/view-controls.css b/assets/css/view-controls.css index 9ceece9..b608505 100644 --- a/assets/css/view-controls.css +++ b/assets/css/view-controls.css @@ -2,20 +2,80 @@ VIEW CONTROLS AND LAYOUT MODES ======================================== */ +/* Controls Container */ +.controls-container { + display: flex; + justify-content: space-between; + align-items: center; + margin: 1rem 0; + gap: 2rem; + flex-wrap: wrap; +} + +/* Sort Controls */ +.sort-controls { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: var(--tree-bg); + border: 1px solid var(--border-color); + border-radius: 8px; +} + +.sort-label { + font-size: 0.9rem; + color: var(--text-color); + margin-right: 4px; + font-weight: 500; +} + +.sort-btn, .sort-order-btn { + background: var(--card-bg) !important; + border: 1px solid var(--border-color) !important; + border-radius: 6px !important; + padding: 6px 12px !important; + cursor: pointer !important; + transition: all 0.2s ease !important; + font-size: 0.85rem !important; + color: var(--text-color) !important; + display: flex !important; + align-items: center !important; + gap: 4px !important; + outline: none !important; + text-decoration: none !important; +} + +.sort-btn:hover, .sort-order-btn:hover { + background: var(--button-bg) !important; + color: white !important; + transform: translateY(-1px) !important; + box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; +} + +.sort-btn.active, .sort-order-btn.active { + background: var(--button-bg) !important; + color: white !important; + border-color: var(--button-bg) !important; + box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; +} + +.sort-order-btn { + min-width: 32px !important; + justify-content: center !important; + font-size: 1rem !important; +} + /* View Controls */ .view-controls { display: flex; justify-content: flex-end; align-items: center; gap: 12px; - margin: 1rem 0; padding: 12px 16px; background: var(--tree-bg); border: 1px solid var(--border-color); border-radius: 8px; - width: fit-content; - margin-left: auto; - margin-right: 2rem; position: relative; z-index: 10; } @@ -103,6 +163,13 @@ color: var(--text-color); } +.plot-container .plot-date { + font-size: 0.8rem; + color: var(--text-secondary); + margin-top: 0.3rem; + opacity: 0.7; /* Slightly lower opacity for differentiation */ +} + /* Grid View - Override any conflicting styles */ .plot-container.grid-view { display: grid !important; @@ -199,6 +266,9 @@ flex: 1 !important; padding: 0 !important; text-align: left !important; + display: flex !important; + justify-content: space-between !important; + align-items: center !important; } .plot-container.list-large-view .plot-name { @@ -206,6 +276,16 @@ line-height: 1.4 !important; max-height: none !important; overflow: visible !important; + flex: 1 !important; +} + +.plot-container.list-large-view .plot-date { + flex-shrink: 0 !important; + margin-left: 1rem !important; + margin-top: 0 !important; + font-size: 0.85rem !important; + color: var(--text-secondary) !important; + white-space: nowrap !important; } /* Compact List View */ @@ -237,6 +317,9 @@ padding: 0 !important; flex: 1 !important; text-align: left !important; + display: flex !important; + justify-content: space-between !important; + align-items: center !important; } .plot-container.list-compact-view .plot-name { @@ -246,6 +329,16 @@ white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; + flex: 1 !important; +} + +.plot-container.list-compact-view .plot-date { + flex-shrink: 0 !important; + margin-left: 1rem !important; + margin-top: 0 !important; + font-size: 0.8rem !important; + color: var(--text-secondary) !important; + white-space: nowrap !important; } /* Highlight effect for all views */ diff --git a/assets/css/view-override.css b/assets/css/view-override.css index 315fe0f..e9bd47c 100644 --- a/assets/css/view-override.css +++ b/assets/css/view-override.css @@ -5,8 +5,8 @@ /* Force grid layout when grid-view class is present */ body .plot-container.grid-view { display: grid !important; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)) !important; - gap: 1rem !important; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)) !important; + gap: 1.2rem !important; padding: 1rem 0 !important; } diff --git a/assets/js/folder-metadata.js b/assets/js/folder-metadata.js new file mode 100644 index 0000000..6be3ce4 --- /dev/null +++ b/assets/js/folder-metadata.js @@ -0,0 +1,69 @@ +/** + * Folder Metadata functionality for Gallery + * + * Handles interactions with folder-level metadata display + */ + +// Function to toggle expansion of long metadata text +function toggleMetadataText(button) { + const longText = button.previousElementSibling; + const fullText = button.nextElementSibling; + + if (fullText.style.display === 'none') { + // Show full text + longText.style.display = 'none'; + fullText.style.display = 'inline'; + fullText.classList.add('show'); + button.textContent = 'Show less'; + } else { + // Show truncated text + longText.style.display = 'inline'; + fullText.style.display = 'none'; + fullText.classList.remove('show'); + button.textContent = 'Show more'; + } +} + +// Function to handle folder metadata editing (placeholder for future implementation) +function editFolderMetadata() { + // For now, just show an alert that this feature is coming soon + alert('Folder metadata editing functionality is coming soon!'); + + // Future implementation will: + // 1. Open an edit modal with form fields for each metadata key + // 2. Allow adding/removing metadata fields + // 3. Validate the input + // 4. Send updates to the server + // 5. Refresh the page or update the display dynamically +} + +// Initialize folder metadata functionality when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + console.log('Folder metadata functionality initialized'); + + // Add keyboard shortcuts for metadata editing (future feature) + document.addEventListener('keydown', function(e) { + // Ctrl+M for metadata editing + if (e.ctrlKey && e.key === 'm') { + e.preventDefault(); + const editBtn = document.querySelector('.folder-metadata-edit-btn'); + if (editBtn) { + editFolderMetadata(); + } + } + }); + + // Add accessibility improvements + const metadataItems = document.querySelectorAll('.folder-metadata-item'); + metadataItems.forEach(item => { + item.setAttribute('tabindex', '0'); + item.setAttribute('role', 'listitem'); + }); + + // Add ARIA labels for better accessibility + const metadataContainer = document.querySelector('.folder-metadata-container'); + if (metadataContainer) { + metadataContainer.setAttribute('role', 'region'); + metadataContainer.setAttribute('aria-label', 'Folder metadata information'); + } +}); diff --git a/assets/js/gallery-app.js b/assets/js/gallery-app.js index b24eec6..849286e 100644 --- a/assets/js/gallery-app.js +++ b/assets/js/gallery-app.js @@ -10,6 +10,7 @@ import { ComparisonManager } from './comparison-manager.js'; import { StatsManager } from './stats-manager.js'; import { KeyboardManager } from './keyboard-manager.js'; import { ViewManager } from './view-manager.js'; +import { SortManager } from './sort-manager.js'; import { Utils } from './utils.js'; /** @@ -30,6 +31,7 @@ export class GalleryApp { this.comparisonManager = new ComparisonManager(); this.statsManager = new StatsManager(); this.viewManager = new ViewManager(); + this.sortManager = new SortManager(); this.keyboardManager = new KeyboardManager(this); this.utils = Utils; @@ -39,6 +41,7 @@ export class GalleryApp { window.recentPlotsManager = this.recentPlotsManager; window.comparisonManager = this.comparisonManager; window.viewManager = this.viewManager; + window.sortManager = this.sortManager; window.utils = this.utils; this.init(); @@ -63,7 +66,6 @@ export class GalleryApp { // Backward compatibility methods toggleTheme() { this.themeManager.toggle(); } toggleSidebar() { this.recentPlotsManager.toggleSidebar(); } - refreshGallery() { Utils.refreshGallery(); } toggleCompareMode() { this.comparisonManager.toggleCompareMode(); } closeComparison() { this.comparisonManager.closeComparison(); } selectPlotForComparison(slot) { this.comparisonManager.selectPlotForComparison(slot); } diff --git a/assets/js/keyboard-manager.js b/assets/js/keyboard-manager.js index cc7ad00..c535a9c 100644 --- a/assets/js/keyboard-manager.js +++ b/assets/js/keyboard-manager.js @@ -46,10 +46,24 @@ export class KeyboardManager { } } - if (e.key === 'F5') { + if (e.ctrlKey && e.key === 'n') { e.preventDefault(); - if (this.app.utils && this.app.utils.refreshGallery) { - this.app.utils.refreshGallery(); + if (this.app.sortManager) { + this.app.sortManager.setSortType('name'); + } + } + + if (e.ctrlKey && e.key === 'm') { + e.preventDefault(); + if (this.app.sortManager) { + this.app.sortManager.setSortType('time'); + } + } + + if (e.ctrlKey && e.key === 'o') { + e.preventDefault(); + if (this.app.sortManager) { + this.app.sortManager.toggleSortOrder(); } } diff --git a/assets/js/main.js b/assets/js/main.js index adcfd02..8f924e6 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -10,12 +10,10 @@ let app; // Global functions for onclick handlers (backward compatibility) function toggleTheme() { app.toggleTheme(); } function toggleSidebar() { app.toggleSidebar(); } -function refreshGallery() { app.refreshGallery(); } // Make functions globally available window.toggleTheme = toggleTheme; window.toggleSidebar = toggleSidebar; -window.refreshGallery = refreshGallery; // Initialize application when DOM is ready document.addEventListener('DOMContentLoaded', function() { diff --git a/assets/js/sort-manager.js b/assets/js/sort-manager.js new file mode 100644 index 0000000..8ec9a85 --- /dev/null +++ b/assets/js/sort-manager.js @@ -0,0 +1,197 @@ +/** + * Sort Manager - handles sorting of plot items by name and creation time + */ +export class SortManager { + constructor() { + this.currentSort = 'name'; + this.currentOrder = 'asc'; + this.init(); + } + + /** + * Initialize sort controls + */ + init() { + // Use setTimeout to ensure DOM is ready + setTimeout(() => { + this.setupSortButtons(); + this.loadSavedSort(); + }, 100); + } + + /** + * Setup sort button event listeners + */ + setupSortButtons() { + const sortButtons = document.querySelectorAll('.sort-btn'); + const orderButton = document.querySelector('.sort-order-btn'); + + if (sortButtons.length === 0) { + setTimeout(() => this.setupSortButtons(), 500); + return; + } + + sortButtons.forEach((button) => { + const sortType = button.getAttribute('data-sort'); + + button.addEventListener('click', (e) => { + e.preventDefault(); + this.setSortType(sortType); + }); + }); + + if (orderButton) { + orderButton.addEventListener('click', (e) => { + e.preventDefault(); + this.toggleSortOrder(); + }); + } + + // Initialize button states + this.updateSortButtons(); + this.updateOrderButton(); + } + + /** + * Set the sort type (name or time) + */ + setSortType(sortType) { + if (sortType === this.currentSort) return; + + this.currentSort = sortType; + this.updateSortButtons(); + this.sortPlots(); + this.saveSortPreference(); + } + + /** + * Toggle sort order between ascending and descending + */ + toggleSortOrder() { + this.currentOrder = this.currentOrder === 'asc' ? 'desc' : 'asc'; + this.updateOrderButton(); + this.sortPlots(); + this.saveSortPreference(); + } + + /** + * Update visual state of sort buttons + */ + updateSortButtons() { + const sortButtons = document.querySelectorAll('.sort-btn'); + + sortButtons.forEach(btn => { + if (btn.getAttribute('data-sort') === this.currentSort) { + btn.classList.add('active'); + } else { + btn.classList.remove('active'); + } + }); + } + + /** + * Update visual state of order button + */ + updateOrderButton() { + const orderButton = document.querySelector('.sort-order-btn'); + if (orderButton) { + orderButton.textContent = this.currentOrder === 'asc' ? '↑' : '↓'; + orderButton.setAttribute('data-order', this.currentOrder); + orderButton.title = `Sort Order: ${this.currentOrder === 'asc' ? 'Ascending' : 'Descending'}`; + } + } + + /** + * Sort the plot items + */ + sortPlots() { + const plotContainer = document.getElementById('plotContainer'); + if (!plotContainer) return; + + const plotItems = Array.from(plotContainer.children); + + plotItems.sort((a, b) => { + let valueA, valueB; + + if (this.currentSort === 'name') { + valueA = a.getAttribute('data-name') || ''; + valueB = b.getAttribute('data-name') || ''; + + // Natural sort for better number handling + const result = valueA.localeCompare(valueB, undefined, { + numeric: true, + sensitivity: 'base' + }); + return this.currentOrder === 'asc' ? result : -result; + } else if (this.currentSort === 'time') { + valueA = parseInt(a.getAttribute('data-time') || '0'); + valueB = parseInt(b.getAttribute('data-time') || '0'); + + const result = valueA - valueB; + return this.currentOrder === 'asc' ? result : -result; + } + + return 0; + }); + + // Re-append sorted items + plotItems.forEach(item => { + plotContainer.appendChild(item); + }); + } + + /** + * Save sort preferences to localStorage + */ + saveSortPreference() { + try { + localStorage.setItem('gallery-sort-type', this.currentSort); + localStorage.setItem('gallery-sort-order', this.currentOrder); + } catch (e) { + // Ignore localStorage errors + } + } + + /** + * Load saved sort preferences + */ + loadSavedSort() { + try { + const savedSort = localStorage.getItem('gallery-sort-type'); + const savedOrder = localStorage.getItem('gallery-sort-order'); + + if (savedSort && ['name', 'time'].includes(savedSort)) { + this.currentSort = savedSort; + } + + if (savedOrder && ['asc', 'desc'].includes(savedOrder)) { + this.currentOrder = savedOrder; + } + + this.updateSortButtons(); + this.updateOrderButton(); + + // Sort immediately if there are plots + setTimeout(() => this.sortPlots(), 100); + } catch (e) { + // Ignore localStorage errors, use defaults + } + } + + /** + * Get current sort settings + */ + getCurrentSort() { + return { + type: this.currentSort, + order: this.currentOrder + }; + } + + /** + * Refresh sorting (call this when plot content changes) + */ + refresh() { + this.sortPlots(); + } +} diff --git a/assets/js/utils.js b/assets/js/utils.js index f9817d4..3c5b432 100644 --- a/assets/js/utils.js +++ b/assets/js/utils.js @@ -95,50 +95,6 @@ export class Utils { } } - /** - * Refresh gallery by calling CGI script - */ - static async refreshGallery() { - const refreshBtn = document.getElementById('refreshBtn'); - if (!refreshBtn) return; - - 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(cgiPath, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - } - }); - - if (response.ok) { - refreshBtn.textContent = '✅'; - setTimeout(() => { - window.location.reload(); - }, 1000); - } else { - throw new Error('Refresh failed'); - } - } catch (error) { - console.error('Error refreshing gallery:', error); - refreshBtn.textContent = '❌'; - setTimeout(() => { - refreshBtn.disabled = false; - refreshBtn.textContent = '🔄'; - }, 3000); - } - } - /** * Toggle keyboard shortcuts help display */ diff --git a/assets/js/view-manager.js b/assets/js/view-manager.js index c35381a..0d51208 100644 --- a/assets/js/view-manager.js +++ b/assets/js/view-manager.js @@ -21,12 +21,12 @@ export class ViewManager { */ updateControlsVisibility() { const plotContainer = document.getElementById('plotContainer'); - const viewControls = document.querySelector('.view-controls'); + const controlsContainer = document.querySelector('.controls-container'); - if (!plotContainer || !viewControls) return; + if (!plotContainer || !controlsContainer) return; const hasPlots = plotContainer.children.length > 0; - viewControls.style.display = hasPlots ? 'flex' : 'none'; + controlsContainer.style.display = hasPlots ? 'flex' : 'none'; } /** @@ -34,12 +34,10 @@ export class ViewManager { */ setupViewButtons() { const viewButtons = document.querySelectorAll('.view-btn'); - console.log('ViewManager: Found', viewButtons.length, 'view buttons'); viewButtons.forEach(button => { button.addEventListener('click', (e) => { const newView = button.getAttribute('data-view'); - console.log('ViewManager: Switching to view:', newView); this.switchView(newView); }); }); @@ -49,19 +47,12 @@ export class ViewManager { * Switch to a different view mode */ switchView(viewMode) { - console.log('ViewManager: switchView called with:', viewMode, 'current:', this.currentView); - if (viewMode === this.currentView) return; const plotContainer = document.getElementById('plotContainer'); const viewButtons = document.querySelectorAll('.view-btn'); - if (!plotContainer) { - console.error('ViewManager: plotContainer not found'); - return; - } - - console.log('ViewManager: Plot container found, classes before:', plotContainer.className); + if (!plotContainer) return; // Remove current view class plotContainer.classList.remove( @@ -86,8 +77,6 @@ export class ViewManager { viewMode = 'grid'; } - console.log('ViewManager: Plot container classes after:', plotContainer.className); - // Update button states viewButtons.forEach(btn => { if (btn.getAttribute('data-view') === viewMode) { diff --git a/debug_template.py b/debug_template.py deleted file mode 100644 index 89e9c8a..0000000 --- a/debug_template.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 - -from pathlib import Path -from jinja2 import Environment, FileSystemLoader - -# Test template rendering -env = Environment(loader=FileSystemLoader(".")) -template = env.get_template("templates/gallery.html") - -# Mock data -test_items = [{ - "name": "test_plot", - "pdf_href": "test_plot.pdf", - "png_href": "test_plot.png", - "metadata": {"key1": "value1", "key2": "value2"} -}] - -test_config = { - "paths": {"work_dir": "/work/kschmidt/web"}, - "ui": {"search_debounce_ms": 300, "max_recent_plots": 20} -} - -rendered = template.render( - title="Test Gallery", - items=test_items, - subdirs=["test_subdir"], - relpath="test/path", - paths=test_config["paths"], - ui=test_config["ui"], - stats={"file_count": 1, "folder_count": 1, "total_size": "100 KB", "total_size_bytes": 100000}, - folder_metadata={}, - assets_path="../assets" -) - -# Extract just the metadata button part -lines = rendered.split('\n') -for i, line in enumerate(lines): - if 'metadata-btn' in line: - print(f"Line {i}: {line.strip()}") - if i > 0: - print(f"Line {i-1}: {lines[i-1].strip()}") - if i < len(lines)-1: - print(f"Line {i+1}: {lines[i+1].strip()}") - break - -print("\n--- Assets path in template ---") -for i, line in enumerate(lines): - if 'assets_path' in line: - print(f"Line {i}: {line.strip()}") diff --git a/docs/EXPORT_FUNCTIONALITY.md b/docs/EXPORT_FUNCTIONALITY.md deleted file mode 100644 index 96669e7..0000000 --- a/docs/EXPORT_FUNCTIONALITY.md +++ /dev/null @@ -1,101 +0,0 @@ -# PDF Export Functionality - -The gallery now includes the ability to export multiple plots to a merged PDF with grid layout. - -## Features - -- Select up to 4 plots from any gallery page -- Automatic grid layout (1x1, 1x2, 2x2) -- Export to PDF with proper scaling -- Keyboard shortcuts for easy access - -## Usage - -### Selecting Plots - -1. Press **Ctrl+E** to enter selection mode -2. Click on up to 4 plot thumbnails to select them -3. Selected plots will show a checkmark overlay -4. A counter shows how many plots are selected (e.g., "2/4 selected") - -### Exporting - -1. After selecting plots, click the **📄** export button (appears in floating buttons) -2. The system will show export instructions with: - - JSON data for the export request - - Command to run the export script -3. Copy the JSON data and save it as `export_request.json` -4. Run the export command in your terminal - -### Keyboard Shortcuts - -- **Ctrl+E**: Toggle selection mode -- **Escape**: Clear all selections and exit selection mode - -## Export Methods - -The system supports two PDF merging methods: - -### Method 1: pdfjam (Recommended) -```bash -# Install on Ubuntu/Debian -sudo apt-get install texlive-extra-utils - -# Check if available -python export_plots.py --check-deps -``` - -### Method 2: Python libraries -```bash -# Install Python dependencies -pip install PyPDF2 reportlab - -# Check if available -python export_plots.py --check-deps -``` - -## Export Script Usage - -```bash -# Basic usage -python export_plots.py export_request.json - -# Specify output file -python export_plots.py export_request.json --output my_plots.pdf - -# Check available dependencies -python export_plots.py --check-deps -``` - -## JSON Request Format - -```json -{ - "plots": [ - "/path/to/plot1.pdf", - "/path/to/plot2.pdf" - ], - "layout": { - "rows": 1, - "cols": 2 - }, - "output_name": "merged_plots.pdf" -} -``` - -## Layout Options - -- **1 plot**: 1x1 grid -- **2 plots**: 1x2 grid (horizontal) -- **3 plots**: 2x2 grid (one empty slot) -- **4 plots**: 2x2 grid (full) - -## Technical Details - -The export functionality consists of: - -- **Frontend**: JavaScript selection UI and export manager -- **Backend**: Python script for PDF merging -- **CSS**: Styling for selection mode and overlays - -The system is designed to work without requiring a web server, using file-based communication between the browser and Python script. diff --git a/docs/METADATA_IMPLEMENTATION.md b/docs/METADATA_IMPLEMENTATION.md deleted file mode 100644 index 5bc2950..0000000 --- a/docs/METADATA_IMPLEMENTATION.md +++ /dev/null @@ -1,103 +0,0 @@ -# Metadata System Implementation Summary - -## What Was Implemented - -### 1. Core Metadata Module (`metadata.py`) -- **`load_metadata_file()`**: Loads YAML/JSON metadata files with error handling -- **`load_folder_metadata()`**: Discovers and loads folder-level metadata (meta.yaml/meta.json) -- **`merge_metadata()`**: Merges parent and child metadata with proper override behavior -- **`resolve_metadata_for_plot()`**: Resolves final metadata for individual plots -- **`save_metadata_cache()`**: Saves resolved metadata to cache files for performance - -### 2. Updated Gallery Generator (`generate_gallery.py`) -- **Hierarchical inheritance**: Folder metadata is inherited by subfolders and plots -- **Plot-specific overrides**: Individual plots can have their own metadata files -- **Template integration**: Metadata is passed to HTML templates for rendering -- **Cache generation**: `meta_cache.json` files are created in each output directory - -### 3. Configuration Updates (`config.py` and `config.yaml`) -- Added `MetadataConfig` class with caching and inheritance options -- Updated main `Config` class to include metadata settings -- Added metadata section to `config.yaml` - -### 4. Documentation and Examples -- **`METADATA_USAGE.md`**: Comprehensive documentation on using the metadata system -- **`examples/meta.yaml`**: Example folder metadata file -- **`examples/specific_plot.json`**: Example plot-specific metadata file -- **`validate_metadata.py`**: Utility script for validating metadata files - -## Key Features - -### Hierarchical Metadata Inheritance -``` -root_folder/ -├── meta.yaml # Base metadata for all plots -├── subfolder/ -│ ├── meta.yaml # Inherits from parent, can override -│ ├── plot1.pdf -│ ├── plot1.yaml # Plot-specific metadata -│ └── plot2.pdf # Uses folder metadata -``` - -### Flexible Format Support -- YAML files: `.yaml`, `.yml` -- JSON files: `.json` -- Automatic format detection based on file extension - -### Template Integration -- `folder_metadata`: Available in templates for folder-level metadata -- `item.metadata`: Available for each plot in the items loop -- Clean separation of concerns between data and presentation - -### Performance Optimization -- Metadata caching in `meta_cache.json` files -- Only reload when source files are newer than cache -- Efficient hierarchical resolution - -## Usage Examples - -### Basic Folder Metadata -```yaml -# meta.yaml -title: "Physics Analysis Results" -experiment: "CMS" -author: - name: "Researcher Name" - institution: "University" -tags: ["analysis", "physics"] -``` - -### Plot-specific Metadata -```yaml -# my_plot.yaml (for my_plot.pdf) -title: "Signal Region Analysis" -plot_type: "histogram" -variables: - x_axis: "mass" - y_axis: "events" -highlight: true -``` - -### Template Usage -```html -

{{ folder_metadata.title }}

-{% for item in items %} -
-

{{ item.metadata.title or item.name }}

- {% if item.metadata.plot_type %} - {{ item.metadata.plot_type }} - {% endif %} -
-{% endfor %} -``` - -## Benefits - -1. **Flexibility**: Support any metadata structure using YAML/JSON -2. **Inheritance**: Avoid repetition by inheriting from parent folders -3. **Override capability**: Fine-tune metadata for specific plots -4. **Performance**: Caching system for efficient repeated builds -5. **Validation**: Built-in error handling and validation utilities -6. **Documentation**: Comprehensive usage documentation and examples - -The metadata system is now fully integrated and ready for use in your scientific plot gallery generator! diff --git a/docs/METADATA_USAGE.md b/docs/METADATA_USAGE.md deleted file mode 100644 index 86a55f3..0000000 --- a/docs/METADATA_USAGE.md +++ /dev/null @@ -1,114 +0,0 @@ -# Metadata System Documentation - -## Overview - -The metadata system allows you to add flexible metadata to your plots and folders using YAML or JSON files. Metadata is inherited hierarchically from parent folders and can be overridden at any level. - -## File Structure - -### Folder Metadata -- **File names**: `meta.yaml`, `meta.yml`, or `meta.json` -- **Location**: Place in any folder containing plots -- **Scope**: Applies to all plots in the folder and subfolders (unless overridden) - -### Plot-specific Metadata -- **File names**: `{plot_name}.yaml`, `{plot_name}.yml`, or `{plot_name}.json` -- **Location**: Place in the same folder as the plot PDF file -- **Scope**: Applies only to the specific plot with the same name - -## Hierarchy and Inheritance - -1. **Root folder**: Start with folder metadata in your source directory -2. **Subfolders**: Each subfolder can have its own `meta.yaml` that merges with parent metadata -3. **Plot-specific**: Individual plots can have their own metadata files that override folder metadata - -## Example Usage - -### Folder Structure -``` -analysis_results/ -├── meta.yaml # Root folder metadata -├── signal/ -│ ├── meta.yaml # Signal-specific metadata -│ ├── mass_plot.pdf -│ └── mass_plot.yaml # Plot-specific metadata -└── background/ - ├── meta.yaml # Background-specific metadata - └── qcd_plot.pdf -``` - -### Example Metadata Fields - -**Common fields for folder metadata:** -- `title`: Folder title -- `description`: Folder description -- `experiment`: Experiment name (CMS, ATLAS, etc.) -- `dataset`: Dataset identifier -- `analysis_type`: Type of analysis -- `author`: Author information -- `parameters`: Analysis parameters -- `tags`: Categorization tags - -**Common fields for plot metadata:** -- `plot_type`: Type of plot (histogram, scatter, etc.) -- `variables`: Variable information (x_axis, y_axis, units) -- `selection`: Selection criteria -- `statistics`: Statistical information -- `display`: Display options (highlight, featured, order_priority) - -## Configuration - -The metadata system can be configured in `config.yaml`: - -```yaml -metadata: - cache_enabled: true # Enable metadata caching - inherit_from_parent: true # Enable hierarchical inheritance -``` - -## Output - -### HTML Template -Metadata is available in the HTML template as: -- `folder_metadata`: Current folder's resolved metadata -- `item.metadata`: Individual plot metadata (in items loop) - -### Cache Files -- `meta_cache.json`: Generated in each web directory -- Contains resolved metadata for all plots in that directory -- Used for performance optimization and debugging - -## Usage Tips - -1. **Start simple**: Begin with basic folder metadata and add complexity as needed -2. **Use inheritance**: Put common metadata in parent folders to avoid repetition -3. **Override selectively**: Use plot-specific metadata only when needed -4. **Consistent naming**: Use consistent field names across your metadata files -5. **Validate format**: Ensure YAML/JSON files are valid before running the generator - -## Integration with Templates - -In your HTML templates, you can access metadata like: - -```html - -

{{ folder_metadata.title }}

-

{{ folder_metadata.description }}

- - -{% for item in items %} -
-

{{ item.name }}

- {% if item.metadata.plot_type %} - {{ item.metadata.plot_type }} - {% endif %} - {% if item.metadata.tags %} -
- {% for tag in item.metadata.tags %} - {{ tag }} - {% endfor %} -
- {% endif %} -
-{% endfor %} -``` diff --git a/docs/SORT_IMPLEMENTATION.md b/docs/SORT_IMPLEMENTATION.md new file mode 100644 index 0000000..b0dae4a --- /dev/null +++ b/docs/SORT_IMPLEMENTATION.md @@ -0,0 +1,136 @@ +# Gallery Sort Functionality Implementation Guide + +## Overview +This guide explains how to integrate the new sorting functionality that allows users to sort plots by name and creation time. + +## Frontend Implementation (Complete ✅) + +The frontend implementation is complete and includes: + +### 1. Sort Controls UI +- **Name/Time buttons**: Toggle between sorting by filename and creation time +- **Order button**: Toggle between ascending (↑) and descending (↓) order +- **Positioned**: Left side of the controls container, next to view toggle buttons +- **Responsive**: Adapts to mobile layouts + +### 2. Keyboard Shortcuts +- `Ctrl+N`: Sort by name +- `Ctrl+M`: Sort by time (modification/creation time) +- `Ctrl+O`: Toggle sort order (ascending/descending) + +### 3. Persistence +- Sort preferences are saved to localStorage +- Settings persist across page reloads and navigation + +### 4. Tile Sizing +- Grid view now shows ~6 plots per row on desktop (240px minimum width) +- Responsive design maintains usability on mobile devices + +## Backend Integration (Required) + +To enable time-based sorting, you need to modify your Python gallery generation code: + +### 1. Add Creation Time to Plot Items + +```python +from pathlib import Path + +def add_creation_time_to_items(items, base_path): + """Add creation time to plot items for sorting functionality.""" + for item in items: + try: + # Get creation time from PNG or PDF file + png_path = None + pdf_path = None + + if 'png_href' in item: + png_rel_path = item['png_href'].replace('../', '').replace('./', '') + png_path = Path(base_path) / png_rel_path + + if 'pdf_href' in item: + pdf_rel_path = item['pdf_href'].replace('../', '').replace('./', '') + pdf_path = Path(base_path) / pdf_rel_path + + # Use PNG creation time if available, otherwise PDF + creation_time = 0 + if png_path and png_path.exists(): + creation_time = int(png_path.stat().st_ctime) + elif pdf_path and pdf_path.exists(): + creation_time = int(pdf_path.stat().st_ctime) + + item['creation_time'] = creation_time + + except Exception as e: + print(f"Warning: Could not get creation time for {item.get('name', 'unknown')}: {e}") + item['creation_time'] = 0 + + return items +``` + +### 2. Integrate into Your Gallery Generation + +In your existing gallery generation code, call this function before rendering the template: + +```python +# Your existing code that creates the items list +items = generate_plot_items() # Your existing function + +# Add creation times +items = add_creation_time_to_items(items, gallery_base_path) + +# Pass to template +template.render(items=items, ...) +``` + +### 3. Template Data Structure + +The template now expects each item to have a `creation_time` field: + +```python +item = { + 'name': 'plot_name.png', + 'png_href': './plot_name.png', + 'pdf_href': './plot_name.pdf', + 'creation_time': 1642723200 # Unix timestamp +} +``` + +## File Locations + +### Frontend Files (Ready to use) +- `templates/gallery.html` - Updated with sort controls and data attributes +- `assets/css/view-controls.css` - Styling for sort and view controls +- `assets/css/view-override.css` - Grid layout with larger tiles +- `assets/js/sort-manager.js` - Sort functionality implementation +- `assets/js/gallery-app.js` - Integration of SortManager +- `assets/js/keyboard-manager.js` - Keyboard shortcuts for sorting + +### Backend Integration +- `python/add_creation_time.py` - Example implementation for adding creation times + +## Features Summary + +### ✅ Completed Features +1. **Larger Grid Tiles**: ~6 plots per row instead of 8 +2. **Sort Controls**: Name and time sorting with visual feedback +3. **Sort Order Toggle**: Ascending/descending with visual indicator +4. **Keyboard Shortcuts**: Quick access to all sort functions +5. **Persistence**: Settings saved across sessions +6. **Responsive Design**: Works on all screen sizes +7. **Template Integration**: Data attributes ready for backend + +### 🔄 Next Steps (Backend Integration) +1. Modify your Python gallery generation code to include `creation_time` +2. Use the provided `add_creation_time_to_items()` function +3. Test with real plot files to ensure timestamps are correct + +## Testing + +After backend integration: +1. Navigate to a gallery with multiple plots +2. Click the sort buttons to verify functionality +3. Use keyboard shortcuts to test responsiveness +4. Check that sort order toggles correctly +5. Verify settings persist after page reload + +The frontend is fully functional and will work immediately once the backend provides the `creation_time` data. diff --git a/examples/metadata.json b/examples/metadata.json new file mode 100644 index 0000000..61751f9 --- /dev/null +++ b/examples/metadata.json @@ -0,0 +1,19 @@ +{ + "title": "Sample Research Project", + "description": "This folder contains plots and analysis results for our sample research project studying data visualization techniques.", + "author": "Research Team", + "created": "2025-01-15", + "project_id": "PROJ-2025-001", + "status": "Active", + "tags": ["data-science", "visualization", "research"], + "collaborators": ["Alice Smith", "Bob Johnson", "Carol Davis"], + "funding": { + "agency": "National Science Foundation", + "grant_number": "NSF-2025-12345", + "amount": "$150,000" + }, + "contact": "research-team@example.com", + "repository": "https://github.com/example/sample-project", + "documentation": "https://docs.example.com/sample-project", + "notes": "This is a long note that demonstrates how the system handles lengthy text content. It should be truncated in the display and allow users to expand it to see the full content. This helps keep the interface clean while still providing access to detailed information when needed." +} diff --git a/export_api.py b/export_api.py deleted file mode 100644 index c2385f7..0000000 --- a/export_api.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -Simple Flask API for PDF Export - -This provides a web API endpoint for the gallery export functionality. -""" - -from flask import Flask, request, jsonify, send_file -from pathlib import Path -import json -import tempfile -import os -from orchestration.pdf_export import PDFExporter, check_dependencies - -app = Flask(__name__) -exporter = PDFExporter() - - -@app.route('/api/export-pdf', methods=['POST']) -def export_pdf(): - """Export selected plots to merged PDF""" - try: - data = request.get_json() - - if not data or 'plots' not in data: - return jsonify({'error': 'No plots specified'}), 400 - - plot_urls = data['plots'] - layout = data.get('layout', {'rows': 2, 'cols': 2}) - - # Convert URLs to file paths - plot_paths = [] - for url in plot_urls: - # Extract path from file:// URL or relative path - if url.startswith('file://'): - path = url[7:] # Remove 'file://' prefix - elif url.startswith('http'): - return jsonify({'error': 'Remote URLs not supported'}), 400 - else: - # Assume relative path from web root - path = url - - # Resolve to absolute path - plot_path = Path(path) - if not plot_path.exists(): - return jsonify({'error': f'Plot not found: {path}'}), 404 - - plot_paths.append(str(plot_path)) - - if not plot_paths: - return jsonify({'error': 'No valid plots found'}), 400 - - # Create merged PDF - pdf_bytes = exporter.merge_plots(plot_paths, layout) - - # Create temporary file to serve - with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp_file: - tmp_file.write(pdf_bytes) - tmp_path = tmp_file.name - - def cleanup_temp_file(): - """Clean up temporary file after sending""" - try: - os.unlink(tmp_path) - except OSError: - pass - - return send_file( - tmp_path, - as_attachment=True, - download_name='merged_plots.pdf', - mimetype='application/pdf' - ) - - except Exception as e: - return jsonify({'error': str(e)}), 500 - - -@app.route('/api/export-status', methods=['GET']) -def export_status(): - """Check export capabilities""" - deps = check_dependencies() - return jsonify({ - 'available': any(deps.values()), - 'dependencies': deps, - 'methods': { - 'pypdf2': deps['pypdf2'], - 'pdfjam': deps['pdfjam'] - } - }) - - -if __name__ == '__main__': - # For development only - app.run(debug=True, port=5000) diff --git a/export_plots.py b/export_plots.py deleted file mode 100644 index 4c4fb0a..0000000 --- a/export_plots.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python3 -""" -Standalone PDF Export Script - -This script processes export requests from the gallery and creates merged PDFs. -Usage: python export_plots.py [request_file.json] -""" - -import sys -import json -import argparse -from pathlib import Path -from orchestration.pdf_export import PDFExporter, check_dependencies - - -def main(): - parser = argparse.ArgumentParser(description='Export gallery plots to merged PDF') - parser.add_argument('request_file', nargs='?', default='export_request.json', - help='JSON file containing export request') - parser.add_argument('--output', '-o', help='Output PDF file path') - parser.add_argument('--check-deps', action='store_true', - help='Check available dependencies') - - args = parser.parse_args() - - if args.check_deps: - deps = check_dependencies() - print("Available dependencies:") - for dep, available in deps.items(): - status = "✓" if available else "✗" - print(f" {status} {dep}") - - if not any(deps.values()): - print("\nNo PDF merging tools available!") - print("Install one of the following:") - print(" - PyPDF2 and reportlab: pip install PyPDF2 reportlab") - print(" - pdfjam: apt-get install texlive-extra-utils (on Ubuntu/Debian)") - return - - request_file = Path(args.request_file) - if not request_file.exists(): - print(f"Error: Request file '{request_file}' not found") - print("Create a JSON file with the following structure:") - print(json.dumps({ - "plots": ["/path/to/plot1.pdf", "/path/to/plot2.pdf"], - "layout": {"rows": 1, "cols": 2}, - "output_name": "merged_plots.pdf" - }, indent=2)) - return 1 - - try: - with open(request_file, 'r') as f: - request_data = json.load(f) - except json.JSONDecodeError as e: - print(f"Error: Invalid JSON in '{request_file}': {e}") - return 1 - - # Validate request data - if 'plots' not in request_data: - print("Error: Missing 'plots' field in request") - return 1 - - plot_paths = request_data['plots'] - layout = request_data.get('layout', {'rows': 2, 'cols': 2}) - output_name = args.output or request_data.get('output_name', 'merged_plots.pdf') - - # Validate plot files exist - missing_files = [] - for plot_path in plot_paths: - if not Path(plot_path).exists(): - missing_files.append(plot_path) - - if missing_files: - print("Error: The following plot files were not found:") - for missing in missing_files: - print(f" - {missing}") - return 1 - - print(f"Exporting {len(plot_paths)} plots to '{output_name}'...") - print(f"Layout: {layout['rows']}x{layout['cols']}") - - # Create exporter and merge plots - exporter = PDFExporter() - - try: - pdf_bytes = exporter.merge_plots(plot_paths, layout, output_name) - - if not args.output and 'output_name' not in request_data: - # Write to file if not already done by exporter - with open(output_name, 'wb') as f: - f.write(pdf_bytes) - - print(f"✓ Successfully created '{output_name}'") - print(f" File size: {len(pdf_bytes) / 1024:.1f} KB") - - # Clean up request file if export was successful - request_file.unlink(missing_ok=True) - - except Exception as e: - print(f"Error during export: {e}") - return 1 - - return 0 - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/generate_gallery.py b/generate_gallery.py index 65b622c..3eae8b5 100644 --- a/generate_gallery.py +++ b/generate_gallery.py @@ -19,6 +19,7 @@ import os import sys from pathlib import Path from typing import Dict, Any, Optional +from datetime import datetime from jinja2 import Environment, FileSystemLoader from orchestration.config import Config @@ -32,7 +33,20 @@ from orchestration.metadata import ( CONFIG = Config.from_yaml("config.yaml") + +def datetime_from_timestamp(timestamp): + """Convert a Unix timestamp to a datetime object.""" + return datetime.fromtimestamp(timestamp) + + +def strftime_filter(dt, fmt): + """Format a datetime object using strftime.""" + return dt.strftime(fmt) + + env = Environment(loader=FileSystemLoader(".")) +env.filters['datetime_from_timestamp'] = datetime_from_timestamp +env.filters['strftime'] = strftime_filter template = env.get_template("templates/gallery.html") @@ -147,11 +161,15 @@ def build_gallery(source_dir: Path, web_dir: Path, plot_metadata = resolve_metadata_for_plot(pdf_file, current_metadata) plot_metadata_cache[pdf_file.stem] = plot_metadata + # Get source file creation time (in seconds since epoch) + source_creation_time = int(pdf_file.stat().st_ctime) + items.append({ "name": pdf_file.stem, "pdf_href": pdf_file.name, "png_href": png_file.name, - "metadata": plot_metadata + "metadata": plot_metadata, + "creation_time": source_creation_time }) # Save metadata cache for this directory @@ -357,10 +375,14 @@ def main(clean_first: bool = False) -> None: else: print(f"Skipping {source_png_path.name} (up to date)") + # Get source file creation time for single file + source_creation_time = int(source_path.stat().st_ctime) + items = [{ "name": source_path.stem, "pdf_href": pdf_name, - "png_href": png_name + "png_href": png_name, + "creation_time": source_creation_time }] # Calculate statistics for single file diff --git a/orchestration/metadata.py b/orchestration/metadata.py index 4c1d9b8..a6bfb84 100644 --- a/orchestration/metadata.py +++ b/orchestration/metadata.py @@ -50,7 +50,7 @@ def load_metadata_file(metadata_path: Path) -> Dict[str, Any]: def load_folder_metadata(folder_path: Path) -> Dict[str, Any]: """ - Load folder-level metadata from meta.yaml or meta.json. + Load folder-level metadata from meta.yaml, meta.json, or metadata.json. Args: folder_path: Path to the folder to check for metadata @@ -58,8 +58,8 @@ def load_folder_metadata(folder_path: Path) -> Dict[str, Any]: Returns: Dictionary containing the folder metadata """ - # Try YAML first, then JSON - for filename in ['meta.yaml', 'meta.yml', 'meta.json']: + # Try YAML first, then JSON (including metadata.json for backwards compatibility) + for filename in ['meta.yaml', 'meta.yml', 'meta.json', 'metadata.json']: metadata_path = folder_path / filename if metadata_path.exists(): return load_metadata_file(metadata_path) diff --git a/python/add_creation_time.py b/python/add_creation_time.py new file mode 100644 index 0000000..135d807 --- /dev/null +++ b/python/add_creation_time.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +""" +Gallery Creation Time Integration +Example function to add creation time metadata to plot items. +This should be integrated into your existing Python gallery generation code. +""" + +from pathlib import Path + + +def add_creation_time_to_items(items, base_path): + """ + Add creation time to plot items for sorting functionality. + + Args: + items: List of plot item dictionaries + base_path: Base path where plot files are located + + Returns: + Updated items list with creation_time field + """ + for item in items: + try: + # Try to get creation time from PNG file first, then PDF + png_path = None + pdf_path = None + + # Extract relative path from href + if 'png_href' in item: + png_rel_path = item['png_href'].replace('../', '').replace( + './', '') + png_path = Path(base_path) / png_rel_path + + if 'pdf_href' in item: + pdf_rel_path = item['pdf_href'].replace('../', '').replace( + './', '') + pdf_path = Path(base_path) / pdf_rel_path + + # Use PNG creation time if available, otherwise PDF + creation_time = 0 + if png_path and png_path.exists(): + creation_time = int(png_path.stat().st_ctime) + elif pdf_path and pdf_path.exists(): + creation_time = int(pdf_path.stat().st_ctime) + + # Add creation time as timestamp (JavaScript can handle this) + item['creation_time'] = creation_time + + except Exception as e: + # Fallback to 0 if there's any error + name = item.get('name', 'unknown') + print(f"Warning: Could not get creation time for {name}: {e}") + item['creation_time'] = 0 + + return items + + +def example_integration(): + """ + Example of how to integrate this into your existing gallery generation. + """ + # This would be part of your existing gallery generation code + items = [ + { + 'name': 'plot1.png', + 'png_href': './plot1.png', + 'pdf_href': './plot1.pdf' + }, + { + 'name': 'plot2.png', + 'png_href': './plot2.png', + 'pdf_href': './plot2.pdf' + } + ] + + base_path = "/path/to/your/gallery/directory" + + # Add creation times + items_with_time = add_creation_time_to_items(items, base_path) + + # Now items_with_time can be passed to your Jinja2 template + # The template will have access to item.creation_time for each item + + return items_with_time + + +if __name__ == "__main__": + # Test the function + items = example_integration() + for item in items: + created = item['creation_time'] + print(f"Plot: {item['name']}, Created: {created}") diff --git a/python/add_metadata.py b/python/add_metadata.py new file mode 100644 index 0000000..cb2d5ef --- /dev/null +++ b/python/add_metadata.py @@ -0,0 +1,45 @@ +from typing import Dict, Any +import json +from pathlib import Path + + +def open_metadata(path: str, filename: str = "metadata.json") -> Dict[str, Any]: + """ + Open metadata file and return its contents as a dictionary. + + Args: + path: The directory path where the metadata file is located. + filename: The name of the metadata file (default: "metadata.json"). + + Returns: + A dictionary containing the metadata. + """ + with open(Path(path) / filename, 'r', encoding='utf-8') as f: + try: + return json.load(f) + except json.JSONDecodeError as e: + raise ValueError(f"Error decoding JSON from {filename}: {e}") + except FileNotFoundError: + raise FileNotFoundError(f"Metadata file {filename} not found in {path}") + except Exception as e: + raise RuntimeError(f"Unexpected error reading metadata: {e}") + + +def merge_metadata( + base_metadata: Dict[str, Any], + additional_metadata: Dict[str, Any] +) -> Dict[str, Any]: + """ + Merge two metadata dictionaries. + + Args: + base_metadata: The base metadata dictionary. + additional_metadata: The additional metadata dictionary to merge. + + Returns: + A new dictionary containing the merged metadata. + """ + merged = base_metadata.copy() + for key, value in additional_metadata.items(): + merged[key] = value + return merged diff --git a/templates/gallery.html b/templates/gallery.html index 927df73..a7d3d07 100644 --- a/templates/gallery.html +++ b/templates/gallery.html @@ -34,50 +34,118 @@
+ + {% if folder_metadata %} +
+ + +
+ {% endif %} + {% if items %} -
- - - +
+
+ + + + +
+
+ + + +
{% endif %}
{% for item in items %} -
+
{{ item.name }}
{{ item.name }}
+
+ {% if item.creation_time and item.creation_time|int > 0 %} + {{ item.creation_time|int|datetime_from_timestamp|strftime('%Y-%m-%d') }} + {% else %} + Unknown + {% endif %} +
{% endfor %} @@ -118,9 +186,6 @@ -
@@ -146,10 +211,6 @@ Exit selection mode Esc
-
- Refresh - F5 -
Toggle theme Ctrl+T @@ -158,6 +219,18 @@ Toggle view Ctrl+V
+
+ Sort by name + Ctrl+N +
+
+ Sort by time + Ctrl+M +
+
+ Toggle sort order + Ctrl+O +
Help ? @@ -236,6 +309,9 @@ + + + diff --git a/test_metadata.html b/test_metadata.html deleted file mode 100644 index e69de29..0000000