Added so much stuff I cant keep up. I think some sorting capability and a way to add metadata to each folder (WIP)

This commit is contained in:
Kylian Schmidt
2025-07-29 09:38:28 +02:00
parent 3e2aaa4be8
commit f3adbe49fa
30 changed files with 1134 additions and 697 deletions
+6
View File
@@ -0,0 +1,6 @@
{
"flake8.args": [
"--max-line-length=120",
"--ignore=W293,E123,W503",
],
}
-15
View File
@@ -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
======================================== */
+203
View File
@@ -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;
}
}
+4
View File
@@ -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');
+11
View File
@@ -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 {
+88
View File
@@ -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;
}
View File
+97 -4
View File
@@ -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 */
+2 -2
View File
@@ -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;
}
+69
View File
@@ -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');
}
});
+3 -1
View File
@@ -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); }
+17 -3
View File
@@ -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();
}
}
-2
View File
@@ -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() {
+197
View File
@@ -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();
}
}
-44
View File
@@ -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
*/
+4 -15
View File
@@ -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) {
-49
View File
@@ -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()}")
-101
View File
@@ -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.
-103
View File
@@ -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
<h1>{{ folder_metadata.title }}</h1>
{% for item in items %}
<div class="plot">
<h3>{{ item.metadata.title or item.name }}</h3>
{% if item.metadata.plot_type %}
<span class="type">{{ item.metadata.plot_type }}</span>
{% endif %}
</div>
{% 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!
-114
View File
@@ -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 -->
<h2>{{ folder_metadata.title }}</h2>
<p>{{ folder_metadata.description }}</p>
<!-- Plot metadata -->
{% for item in items %}
<div class="plot-item">
<h3>{{ item.name }}</h3>
{% if item.metadata.plot_type %}
<span class="plot-type">{{ item.metadata.plot_type }}</span>
{% endif %}
{% if item.metadata.tags %}
<div class="tags">
{% for tag in item.metadata.tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
```
+136
View File
@@ -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.
+19
View File
@@ -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."
}
-94
View File
@@ -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)
-107
View File
@@ -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())
+24 -2
View File
@@ -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
+3 -3
View File
@@ -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)
+92
View File
@@ -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}")
+45
View File
@@ -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
+114 -38
View File
@@ -34,50 +34,118 @@
<!-- Folder Tree -->
<div class="folder-tree" id="folderTree"></div>
<!-- Folder Metadata Dropdown -->
{% if folder_metadata %}
<div class="folder-metadata-container">
<button class="folder-metadata-toggle" onclick="toggleFolderMetadata()">
<span class="folder-metadata-icon">📋</span>
<span class="folder-metadata-label">Folder Information</span>
<span class="folder-metadata-arrow"></span>
</button>
<div class="folder-metadata-content" id="folderMetadataContent">
<div class="folder-metadata-grid">
{% for key, value in folder_metadata.items() %}
<div class="folder-metadata-item">
<span class="folder-metadata-key">{{ key }}:</span>
<span class="folder-metadata-value">
{% if value is string and (value.startswith('http://') or value.startswith('https://')) %}
<a href="{{ value }}" target="_blank" rel="noopener noreferrer">{{ value }}</a>
{% elif value is string and value|length > 100 %}
<span class="folder-metadata-long-text">{{ value[:100] }}...</span>
<button class="folder-metadata-expand" onclick="toggleMetadataText(this)">Show more</button>
<span class="folder-metadata-full-text" style="display: none;">{{ value }}</span>
{% elif value is iterable and value is not string and value is not mapping %}
<div class="folder-metadata-list">
{% for item in value %}
<span class="folder-metadata-tag">{{ item }}</span>
{% endfor %}
</div>
{% elif value is mapping %}
<div class="folder-metadata-nested">
{% for subkey, subvalue in value.items() %}
<div class="folder-metadata-nested-item">
<strong>{{ subkey }}:</strong> {{ subvalue }}
</div>
{% endfor %}
</div>
{% else %}
{{ value }}
{% endif %}
</span>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<!-- View Toggle Controls - Only show if there are plot items -->
{% if items %}
<div class="view-controls">
<button class="view-btn active" data-view="grid" title="Grid View">
<svg width="16" height="16" viewBox="0 0 16 16">
<rect x="1" y="1" width="6" height="6" fill="currentColor"/>
<rect x="9" y="1" width="6" height="6" fill="currentColor"/>
<rect x="1" y="9" width="6" height="6" fill="currentColor"/>
<rect x="9" y="9" width="6" height="6" fill="currentColor"/>
</svg>
</button>
<button class="view-btn" data-view="list-large" title="Large List View">
<svg width="16" height="16" viewBox="0 0 16 16">
<rect x="1" y="2" width="4" height="3" fill="currentColor"/>
<rect x="7" y="2" width="8" height="1" fill="currentColor"/>
<rect x="7" y="4" width="6" height="1" fill="currentColor"/>
<rect x="1" y="7" width="4" height="3" fill="currentColor"/>
<rect x="7" y="7" width="8" height="1" fill="currentColor"/>
<rect x="7" y="9" width="6" height="1" fill="currentColor"/>
<rect x="1" y="12" width="4" height="3" fill="currentColor"/>
<rect x="7" y="12" width="8" height="1" fill="currentColor"/>
<rect x="7" y="14" width="6" height="1" fill="currentColor"/>
</svg>
</button>
<button class="view-btn" data-view="list-compact" title="Compact List View">
<svg width="16" height="16" viewBox="0 0 16 16">
<rect x="1" y="3" width="14" height="1" fill="currentColor"/>
<rect x="1" y="6" width="14" height="1" fill="currentColor"/>
<rect x="1" y="9" width="14" height="1" fill="currentColor"/>
<rect x="1" y="12" width="14" height="1" fill="currentColor"/>
</svg>
</button>
<div class="controls-container">
<div class="sort-controls">
<label class="sort-label">Sort by:</label>
<button class="sort-btn active" data-sort="name" title="Sort by Name">
📝 Name
</button>
<button class="sort-btn" data-sort="time" title="Sort by Creation Time">
🕒 Time
</button>
<button class="sort-order-btn" data-order="asc" title="Sort Order">
</button>
</div>
<div class="view-controls">
<button class="view-btn active" data-view="grid" title="Grid View">
<svg width="16" height="16" viewBox="0 0 16 16">
<rect x="1" y="1" width="6" height="6" fill="currentColor"/>
<rect x="9" y="1" width="6" height="6" fill="currentColor"/>
<rect x="1" y="9" width="6" height="6" fill="currentColor"/>
<rect x="9" y="9" width="6" height="6" fill="currentColor"/>
</svg>
</button>
<button class="view-btn" data-view="list-large" title="Large List View">
<svg width="16" height="16" viewBox="0 0 16 16">
<rect x="1" y="2" width="4" height="3" fill="currentColor"/>
<rect x="7" y="2" width="8" height="1" fill="currentColor"/>
<rect x="7" y="4" width="6" height="1" fill="currentColor"/>
<rect x="1" y="7" width="4" height="3" fill="currentColor"/>
<rect x="7" y="7" width="8" height="1" fill="currentColor"/>
<rect x="7" y="9" width="6" height="1" fill="currentColor"/>
<rect x="1" y="12" width="4" height="3" fill="currentColor"/>
<rect x="7" y="12" width="8" height="1" fill="currentColor"/>
<rect x="7" y="14" width="6" height="1" fill="currentColor"/>
</svg>
</button>
<button class="view-btn" data-view="list-compact" title="Compact List View">
<svg width="16" height="16" viewBox="0 0 16 16">
<rect x="1" y="3" width="14" height="1" fill="currentColor"/>
<rect x="1" y="6" width="14" height="1" fill="currentColor"/>
<rect x="1" y="9" width="14" height="1" fill="currentColor"/>
<rect x="1" y="12" width="14" height="1" fill="currentColor"/>
</svg>
</button>
</div>
</div>
{% endif %}
<!-- Plot Container -->
<div class="plot-container grid-view" id="plotContainer">
{% for item in items %}
<div class="plot-item grid-item">
<div class="plot-item grid-item"
data-name="{{ item.name }}"
data-time="{{ item.creation_time|default(0) }}">
<a href="{{ item.pdf_href }}" class="plot-link">
<img src="{{ item.png_href }}" alt="{{ item.name }}" class="plot-thumbnail">
</a>
<div class="plot-info">
<div class="plot-name" title="{{ item.name }}">{{ item.name }}</div>
<div class="plot-date" title="Created: {{ item.creation_time|default(0)|int|datetime_from_timestamp|strftime('%Y-%m-%d %H:%M') if item.creation_time and item.creation_time|int > 0 else 'Unknown' }}">
{% if item.creation_time and item.creation_time|int > 0 %}
{{ item.creation_time|int|datetime_from_timestamp|strftime('%Y-%m-%d') }}
{% else %}
Unknown
{% endif %}
</div>
</div>
</div>
{% endfor %}
@@ -118,9 +186,6 @@
<button class="floating-btn theme-toggle" onclick="toggleTheme()" id="themeToggle" title="Toggle Theme (Ctrl+T)">
☀️
</button>
<button class="floating-btn refresh-btn" onclick="refreshGallery()" id="refreshBtn" title="Regenerate Gallery (F5)">
🔄
</button>
</div>
<!-- Keyboard Shortcuts Help -->
@@ -146,10 +211,6 @@
<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>
</div>
<div class="shortcut-item">
<span>Toggle theme</span>
<span class="shortcut-key">Ctrl+T</span>
@@ -158,6 +219,18 @@
<span>Toggle view</span>
<span class="shortcut-key">Ctrl+V</span>
</div>
<div class="shortcut-item">
<span>Sort by name</span>
<span class="shortcut-key">Ctrl+N</span>
</div>
<div class="shortcut-item">
<span>Sort by time</span>
<span class="shortcut-key">Ctrl+M</span>
</div>
<div class="shortcut-item">
<span>Toggle sort order</span>
<span class="shortcut-key">Ctrl+O</span>
</div>
<div class="shortcut-item">
<span>Help</span>
<span class="shortcut-key">?</span>
@@ -236,6 +309,9 @@
<!-- Metadata Popup Script -->
<script src="{{ assets_path }}/js/metadata-popup.js"></script>
<!-- Folder Metadata Script -->
<script src="{{ assets_path }}/js/folder-metadata.js"></script>
<!-- Main JavaScript Application -->
<script type="module" src="{{ assets_path }}/js/main.js"></script>
</body>
View File