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:
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"flake8.args": [
|
||||
"--max-line-length=120",
|
||||
"--ignore=W293,E123,W503",
|
||||
],
|
||||
}
|
||||
@@ -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
|
||||
======================================== */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Executable → Regular
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
@@ -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); }
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()}")
|
||||
@@ -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.
|
||||
@@ -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!
|
||||
@@ -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 %}
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user