Move jv and css to own folder

This commit is contained in:
Kylian Schmidt
2025-07-08 08:14:10 +02:00
parent 71cf2031e0
commit 716e1c95fe
25 changed files with 2355 additions and 13 deletions
+137 -5
View File
@@ -1,9 +1,141 @@
# Automated web-based plotting tool
# Gallery Application - Restructured
## Installation
This document explains the new modular structure of the gallery application.
Requires `Jinja2`
## Project Structure
## Config
```
web/
├── assets/
│ ├── css/
│ │ ├── main.css # Main CSS file (imports all others)
│ │ ├── variables.css # CSS custom properties and themes
│ │ ├── base.css # Base layout and typography
│ │ ├── navigation.css # Breadcrumb and navigation styles
│ │ ├── search.css # Search functionality styles
│ │ ├── folder-tree.css # Folder tree component styles
│ │ ├── grid.css # Plot grid and selection styles
│ │ ├── sidebar.css # Recent plots sidebar styles
│ │ ├── floating-elements.css # Floating buttons and help
│ │ ├── stats.css # Gallery statistics styles
│ │ ├── comparison.css # Plot comparison overlay styles
│ │ └── responsive.css # Mobile and responsive styles
│ └── js/
│ ├── main.js # Main entry point
│ ├── gallery-app.js # Main application orchestrator
│ ├── theme-manager.js # Theme switching functionality
│ ├── navigation-manager.js # Breadcrumb and folder tree
│ ├── search-manager.js # Search functionality
│ ├── recent-plots-manager.js # Recent plots sidebar
│ ├── comparison-manager.js # Plot comparison features
│ ├── stats-manager.js # Gallery statistics
│ ├── keyboard-manager.js # Keyboard shortcuts
│ └── utils.js # Utility functions
├── templates/
│ └── gallery.html # Clean HTML template
├── template.html # Original monolithic file (backup)
├── config.py
├── config.yaml
├── generate_gallery.py
└── README.md
```
## Usage
## Key Improvements
### 1. **Separation of Concerns**
- **CSS**: Organized into logical components (navigation, search, grid, etc.)
- **JavaScript**: Split into focused managers with single responsibilities
- **HTML**: Clean template focusing on structure
### 2. **Modular Architecture**
- Each JavaScript module handles a specific feature area
- Modules can be independently maintained and tested
- Clear dependencies and interfaces between modules
### 3. **Maintainability**
- Individual files are much smaller and focused
- Easy to locate and modify specific functionality
- Reduced cognitive load when working on features
### 4. **Development Benefits**
- Better IDE support with syntax highlighting and intellisense
- Easier debugging with source maps
- Ability to add build tools if needed
## Module Responsibilities
### CSS Modules
- **variables.css**: Theme colors and CSS custom properties
- **base.css**: Typography, basic layout, list styles
- **navigation.css**: Breadcrumb and navigation button styles
- **search.css**: Search box, results, and highlighting
- **folder-tree.css**: Collapsible folder tree display
- **grid.css**: Plot thumbnails grid and selection states
- **sidebar.css**: Recent plots sidebar and overlay
- **floating-elements.css**: Action buttons and keyboard help
- **stats.css**: Gallery statistics display
- **comparison.css**: Plot comparison overlay
- **responsive.css**: Mobile and tablet adaptations
### JavaScript Modules
- **ThemeManager**: Light/dark theme switching and persistence
- **NavigationManager**: Breadcrumb building and folder tree construction
- **SearchManager**: Plot search with debouncing and results display
- **RecentPlotsManager**: Recent plots tracking and sidebar management
- **ComparisonManager**: Plot comparison functionality
- **StatsManager**: Gallery statistics calculation and display
- **KeyboardManager**: Keyboard shortcuts and escape handling
- **Utils**: File size formatting, thumbnail highlighting, gallery refresh
### Main Application
- **GalleryApp**: Orchestrates all managers and provides unified interface
- **main.js**: Entry point that initializes the application
## Usage
The restructured application maintains **full backward compatibility** with the original template. All existing functionality works exactly the same way.
### For Python Backend
Update your template reference to use the new template:
```python
# Instead of template.html, use:
template_path = 'templates/gallery.html'
```
### CSS Asset Path
The template expects CSS/JS assets to be served from `/assets/` relative to the gallery pages. Update your web server configuration to serve these static files.
### No Breaking Changes
- All onclick handlers work the same
- All CSS classes remain unchanged
- All IDs and functionality preserved
- Jinja2 template variables work identically
## Development Workflow
### Adding New Features
1. Identify which manager should handle the new functionality
2. Add methods to the appropriate manager class
3. Update the main GalleryApp class if needed
4. Add any new CSS to the appropriate CSS module
### Modifying Existing Features
1. Locate the relevant manager (theme, search, navigation, etc.)
2. Make changes to the specific module
3. Test that the feature works as expected
### Styling Changes
1. Identify the component being styled
2. Edit the appropriate CSS module
3. The main.css file will automatically include changes
## Benefits of This Structure
1. **Easier Debugging**: Each feature is isolated in its own file
2. **Better Performance**: Browser can cache individual modules
3. **Team Development**: Multiple developers can work on different features simultaneously
4. **Code Reuse**: Managers can be reused in other projects
5. **Testing**: Individual modules can be unit tested
6. **Documentation**: Each file has a clear, focused purpose
This restructuring makes the gallery application much more maintainable while preserving all existing functionality.
+54
View File
@@ -0,0 +1,54 @@
/* ========================================
BASE LAYOUT AND TYPOGRAPHY
======================================== */
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 1rem;
padding-bottom: 2rem;
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s;
line-height: 1.5;
}
h1 {
font-size: 1.8rem;
margin-bottom: 0.5rem;
font-weight: 600;
}
h2 {
color: var(--text-color);
font-size: 1.3rem;
margin: 1.5rem 0 0.5rem 0;
}
/* ========================================
SUBDIRECTORIES LIST
======================================== */
ul {
list-style: none;
padding: 0;
}
ul li {
margin: 0.5rem 0;
}
ul li a {
color: var(--link-color);
text-decoration: none;
padding: 0.3rem 0;
display: inline-block;
transition: color 0.2s ease;
}
ul li a:hover {
color: var(--link-hover);
text-decoration: underline;
}
+180
View File
@@ -0,0 +1,180 @@
/* ========================================
PLOT COMPARISON OVERLAY
======================================== */
.comparison-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
z-index: 2000;
display: none;
backdrop-filter: blur(4px);
}
.comparison-overlay.open {
display: flex;
align-items: center;
justify-content: center;
}
.comparison-container {
width: 95%;
height: 90%;
background: var(--bg-color);
border-radius: 12px;
padding: 1.5rem;
display: flex;
flex-direction: column;
position: relative;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
.comparison-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-color);
}
.comparison-title {
font-size: 1.5rem;
font-weight: 600;
color: var(--text-color);
margin: 0;
}
.comparison-close {
background: var(--button-bg);
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 1.2rem;
cursor: pointer;
transition: all 0.2s ease;
}
.comparison-close:hover {
background: var(--button-hover);
transform: scale(1.1);
}
.comparison-content {
flex: 1;
display: flex;
gap: 1rem;
overflow: hidden;
}
.comparison-panel {
flex: 1;
display: flex;
flex-direction: column;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
}
.comparison-panel-header {
background: var(--tree-bg);
padding: 0.8rem 1rem;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.comparison-panel-title {
font-weight: 600;
color: var(--text-color);
font-size: 1rem;
}
.comparison-replace-btn {
background: var(--success-color);
color: white;
border: none;
padding: 0.4rem 0.8rem;
border-radius: 4px;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s ease;
}
.comparison-replace-btn:hover {
background: var(--success-hover);
}
.comparison-panel-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
overflow: auto;
}
.comparison-plot-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.comparison-plot {
max-width: 100%;
max-height: 100%;
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
transition: transform 0.2s ease;
}
.comparison-plot:hover {
transform: scale(1.02);
}
.comparison-placeholder {
width: 100%;
height: 300px;
border: 2px dashed var(--border-color);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: var(--breadcrumb-color);
font-size: 1.1rem;
cursor: pointer;
transition: all 0.2s ease;
}
.comparison-placeholder:hover {
border-color: var(--button-bg);
color: var(--button-bg);
}
.comparison-plot-info {
position: absolute;
bottom: 0.5rem;
left: 0.5rem;
right: 0.5rem;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 0.5rem;
border-radius: 4px;
font-size: 0.9rem;
opacity: 0;
transition: opacity 0.2s ease;
}
.comparison-plot-container:hover .comparison-plot-info {
opacity: 1;
}
+95
View File
@@ -0,0 +1,95 @@
/* ========================================
FLOATING ACTION BUTTONS
======================================== */
.floating-buttons {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 1000;
}
.floating-btn {
width: 56px;
height: 56px;
border-radius: 50%;
border: none;
cursor: pointer;
font-size: 1.3rem;
box-shadow: 0 3px 10px rgba(0,0,0,0.3);
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.floating-btn:hover {
transform: scale(1.1);
}
.sidebar-toggle {
background: var(--button-bg);
color: white;
}
.theme-toggle {
background: var(--button-bg);
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
======================================== */
.shortcuts-help {
position: fixed;
bottom: 140px;
right: 20px;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
display: none;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
z-index: 1000;
font-size: 0.9rem;
max-width: 280px;
}
.shortcuts-help h4 {
margin: 0 0 0.8rem 0;
color: var(--text-color);
font-size: 1rem;
}
.shortcut-item {
display: flex;
justify-content: space-between;
align-items: center;
margin: 0.4rem 0;
}
.shortcut-key {
background: var(--border-color);
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-size: 0.8rem;
font-weight: 500;
}
+40
View File
@@ -0,0 +1,40 @@
/* ========================================
FOLDER TREE
======================================== */
.folder-tree {
background: var(--tree-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
font-size: 0.85rem;
max-height: 350px;
overflow-y: auto;
transition: all 0.3s ease;
}
.tree-item {
margin: 0.2rem 0;
white-space: pre;
font-family: inherit;
}
.tree-current {
background: var(--tree-current-bg);
color: white;
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-weight: 500;
}
.tree-link {
color: var(--link-color);
text-decoration: none;
transition: color 0.2s ease;
}
.tree-link:hover {
text-decoration: underline;
color: var(--link-hover);
}
+106
View File
@@ -0,0 +1,106 @@
/* ========================================
PLOT GRID
======================================== */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1.2rem;
padding: 1rem 0;
}
.grid-item {
text-align: center;
background: var(--card-bg);
border-radius: 8px;
padding: 0.8rem;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.grid-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border-color: var(--border-color);
}
.grid-item img {
max-width: 100%;
border: 1px solid var(--border-color);
border-radius: 6px;
transition: all 0.2s ease;
}
.grid-item a {
color: var(--link-color);
text-decoration: none;
}
.grid-item.highlighted {
border-color: var(--button-bg);
box-shadow: 0 0 15px rgba(0, 120, 212, 0.3);
transform: translateY(-2px);
animation: highlightPulse 2s ease-in-out;
}
@keyframes highlightPulse {
0%, 100% { transform: translateY(-2px) scale(1); }
50% { transform: translateY(-2px) scale(1.02); }
}
.plot-name {
margin-top: 0.8rem;
word-wrap: break-word;
word-break: break-word;
hyphens: auto;
font-size: 0.9rem;
line-height: 1.3;
max-height: 3.9rem;
overflow: hidden;
padding: 0 0.2rem;
font-weight: 500;
}
/* Plot selection mode */
.selecting-plots .grid-item {
cursor: pointer !important;
transition: all 0.2s ease;
position: relative;
}
.selecting-plots .grid-item:hover {
transform: translateY(-4px);
box-shadow: 0 6px 20px rgba(0, 120, 212, 0.3);
border-color: var(--button-bg);
}
.selecting-plots .grid-item::before {
content: '📊 Click to compare';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 120, 212, 0.95);
color: white;
padding: 0.5rem 1rem;
border-radius: 4px;
font-size: 0.9rem;
font-weight: 600;
opacity: 0;
transition: opacity 0.2s ease;
pointer-events: none;
z-index: 10;
white-space: nowrap;
}
.selecting-plots .grid-item:hover::before {
opacity: 1;
}
/* Ensure grid items are clickable in selection mode */
.selecting-plots .grid-item * {
pointer-events: none;
}
.selecting-plots .grid-item {
pointer-events: auto;
}
+20
View File
@@ -0,0 +1,20 @@
/* ========================================
GALLERY STYLES - MAIN ENTRY POINT
======================================== */
/* Core styles */
@import url('./variables.css');
@import url('./base.css');
/* Component styles */
@import url('./navigation.css');
@import url('./search.css');
@import url('./folder-tree.css');
@import url('./grid.css');
@import url('./sidebar.css');
@import url('./floating-elements.css');
@import url('./stats.css');
@import url('./comparison.css');
/* Responsive design */
@import url('./responsive.css');
+57
View File
@@ -0,0 +1,57 @@
/* ========================================
NAVIGATION COMPONENTS
======================================== */
.breadcrumb {
margin: 0.5rem 0 1rem 0;
font-size: 0.9rem;
color: var(--breadcrumb-color);
}
.breadcrumb a {
color: var(--link-color);
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
color: var(--link-hover);
}
.breadcrumb .separator {
margin: 0 0.3rem;
color: var(--breadcrumb-color);
opacity: 0.7;
}
.navigation {
margin: 1rem 0;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.nav-btn {
background: var(--button-bg);
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
cursor: pointer;
text-decoration: none;
transition: all 0.2s ease;
font-size: 0.9rem;
display: inline-flex;
align-items: center;
gap: 0.3rem;
}
.nav-btn:hover {
background: var(--button-hover);
transform: translateY(-1px);
}
.nav-btn:disabled {
background: var(--disabled-color);
cursor: not-allowed;
transform: none;
}
+31
View File
@@ -0,0 +1,31 @@
/* ========================================
RESPONSIVE DESIGN
======================================== */
@media (max-width: 768px) {
body {
padding: 0.5rem;
}
.grid {
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 0.8rem;
}
.sidebar {
width: 100%;
right: -100%;
}
.navigation {
gap: 0.3rem;
}
.nav-btn {
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
}
.search-container {
max-width: 100%;
}
}
+72
View File
@@ -0,0 +1,72 @@
/* ========================================
SEARCH FUNCTIONALITY
======================================== */
.search-container {
position: relative;
max-width: 500px;
margin: 1rem 0;
}
.search-box {
width: 100%;
padding: 0.8rem 3rem 0.8rem 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--card-bg);
color: var(--text-color);
font-size: 1rem;
outline: none;
transition: all 0.3s ease;
}
.search-box:focus {
border-color: var(--button-bg);
box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.1);
}
.search-icon {
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-color);
opacity: 0.6;
pointer-events: none;
}
.search-results {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
margin-top: 0.5rem;
max-height: 400px;
overflow-y: auto;
display: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 100;
}
.search-result-item {
padding: 0.75rem;
margin: 0;
cursor: pointer;
transition: background-color 0.2s ease;
border-bottom: 1px solid var(--border-color);
}
.search-result-item:last-child {
border-bottom: none;
}
.search-result-item:hover {
background: var(--button-bg);
color: white;
}
.search-highlight {
background: #ffeb3b;
color: #000;
font-weight: bold;
padding: 0.1rem 0.2rem;
border-radius: 2px;
}
+121
View File
@@ -0,0 +1,121 @@
/* ========================================
SIDEBAR (RECENT PLOTS)
======================================== */
.sidebar {
position: fixed;
top: 0;
right: -350px;
width: 330px;
height: 100vh;
background: var(--card-bg);
border-left: 2px solid var(--border-color);
z-index: 2000;
transition: right 0.3s ease;
overflow-y: auto;
box-shadow: -4px 0 15px rgba(0,0,0,0.2);
}
.sidebar.open {
right: 0;
}
.sidebar-header {
padding: 1.2rem;
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
background: var(--card-bg);
z-index: 1;
}
.sidebar-title {
margin: 0;
font-size: 1.2rem;
color: var(--text-color);
font-weight: 600;
}
.sidebar-close {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-color);
padding: 0.2rem;
border-radius: 4px;
transition: background-color 0.2s ease;
}
.sidebar-close:hover {
background: var(--border-color);
}
.sidebar-content {
padding: 1rem;
}
.recent-plot {
display: flex;
gap: 0.7rem;
padding: 0.8rem;
margin-bottom: 0.8rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid var(--border-color);
}
.recent-plot:hover {
background: var(--button-bg);
color: white;
transform: translateX(2px);
}
.recent-plot-thumb {
width: 60px;
height: 48px;
object-fit: cover;
border-radius: 4px;
flex-shrink: 0;
}
.recent-plot-info {
flex: 1;
min-width: 0;
}
.recent-plot-name {
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 0.3rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.recent-plot-path {
font-size: 0.8rem;
color: var(--breadcrumb-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1500;
display: none;
backdrop-filter: blur(2px);
}
.sidebar-overlay.open {
display: block;
}
+40
View File
@@ -0,0 +1,40 @@
/* ========================================
GALLERY STATISTICS
======================================== */
.gallery-stats {
position: fixed;
bottom: 20px;
left: 20px;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 0.8rem 1rem;
font-size: 0.85rem;
color: var(--breadcrumb-color);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
z-index: 500;
opacity: 0.8;
transition: opacity 0.2s ease;
max-width: 200px;
}
.gallery-stats:hover {
opacity: 1;
}
.stats-item {
display: flex;
justify-content: space-between;
align-items: center;
margin: 0.2rem 0;
white-space: nowrap;
}
.stats-label {
margin-right: 0.8rem;
}
.stats-value {
font-weight: 600;
color: var(--text-color);
}
+33
View File
@@ -0,0 +1,33 @@
/* ========================================
CSS VARIABLES AND THEME DEFINITIONS
======================================== */
:root {
--bg-color: #1e1e1e;
--text-color: #ffffff;
--card-bg: #2d2d2d;
--border-color: #404040;
--link-color: #569cd6;
--link-hover: #4a9eff;
--button-bg: #0078d4;
--button-hover: #106ebe;
--breadcrumb-color: #cccccc;
--tree-bg: #252526;
--tree-current-bg: #0078d4;
--success-color: #28a745;
--success-hover: #218838;
--disabled-color: #6c757d;
}
[data-theme="light"] {
--bg-color: #ffffff;
--text-color: #333333;
--card-bg: #f8f8f8;
--border-color: #ddd;
--link-color: #007acc;
--link-hover: #005a9e;
--button-bg: #007acc;
--button-hover: #005a9e;
--breadcrumb-color: #666;
--tree-bg: #f8f8f8;
--tree-current-bg: #007acc;
}
+180
View File
@@ -0,0 +1,180 @@
/**
* Plot comparison functionality
*/
export class ComparisonManager {
constructor() {
this.comparisonMode = false;
this.comparisonSlot = null; // 'left' or 'right'
this.plots = { left: null, right: null };
}
/**
* Toggle comparison mode
*/
toggleCompareMode() {
this.comparisonMode = !this.comparisonMode;
const compareBtn = document.getElementById('compareToggle');
if (!compareBtn) return;
if (this.comparisonMode) {
compareBtn.style.background = 'var(--success-color)';
compareBtn.title = 'Exit Compare Mode (Ctrl+C)';
this.showComparisonOverlay();
} else {
compareBtn.style.background = 'var(--button-bg)';
compareBtn.title = 'Compare Plots (Ctrl+C)';
this.hideComparisonOverlay();
}
}
/**
* Show comparison overlay
*/
showComparisonOverlay() {
const overlay = document.getElementById('comparisonOverlay');
if (overlay) overlay.classList.add('open');
}
/**
* Hide comparison overlay
*/
hideComparisonOverlay() {
const overlay = document.getElementById('comparisonOverlay');
if (overlay) overlay.classList.remove('open');
this.comparisonMode = false;
const compareBtn = document.getElementById('compareToggle');
if (compareBtn) {
compareBtn.style.background = 'var(--button-bg)';
compareBtn.title = 'Compare Plots (Ctrl+C)';
}
}
/**
* Close comparison overlay (alias for hideComparisonOverlay)
*/
closeComparison() {
this.hideComparisonOverlay();
}
/**
* Select plot for comparison - simple approach
*/
selectPlotForComparison(slot) {
this.comparisonSlot = slot;
// Hide overlay temporarily by removing the 'open' class
const overlay = document.getElementById('comparisonOverlay');
if (overlay) overlay.classList.remove('open');
// Show simple alert with instructions
const instruction = document.createElement('div');
instruction.id = 'comparisonInstruction';
instruction.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: var(--button-bg);
color: white;
padding: 1rem 2rem;
border-radius: 8px;
z-index: 3000;
font-size: 1.1rem;
font-weight: 600;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
`;
instruction.innerHTML = `📊 Click any plot to select for ${slot === 'left' ? 'Plot A' : 'Plot B'} (ESC to cancel)`;
document.body.appendChild(instruction);
// Add one-time click listener to all grid items
const gridItems = document.querySelectorAll('.grid-item');
const handleClick = (event) => {
event.preventDefault();
event.stopPropagation();
const gridItem = event.currentTarget;
const img = gridItem.querySelector('img');
const nameEl = gridItem.querySelector('.plot-name');
if (img && nameEl) {
const plotInfo = {
name: nameEl.textContent.trim(),
imgSrc: img.src,
pdfSrc: img.src.replace('.png', '.pdf'),
path: window.location.pathname
};
this.addPlotToComparison(plotInfo, slot);
}
// Clean up
instruction.remove();
gridItems.forEach(item => item.removeEventListener('click', handleClick));
this.comparisonSlot = null;
if (overlay) overlay.classList.add('open');
};
gridItems.forEach(item => {
item.style.cursor = 'pointer';
item.style.border = '2px dashed var(--button-bg)';
item.addEventListener('click', handleClick);
});
// ESC to cancel
const handleEscape = (event) => {
if (event.key === 'Escape') {
instruction.remove();
gridItems.forEach(item => {
item.removeEventListener('click', handleClick);
item.style.cursor = '';
item.style.border = '';
});
this.comparisonSlot = null;
if (overlay) overlay.classList.add('open');
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
}
/**
* Add plot to comparison panel
*/
addPlotToComparison(plotInfo, slot) {
this.plots[slot] = plotInfo;
const container = document.getElementById(`${slot}PlotContainer`);
const title = document.getElementById(`${slot}PlotTitle`);
const replaceBtn = document.getElementById(`${slot}ReplaceBtn`);
if (container) {
container.innerHTML = `
<img src="${plotInfo.imgSrc}" class="comparison-plot" alt="${plotInfo.name}"
onclick="window.open('${plotInfo.pdfSrc}', '_blank')" />
<div class="comparison-plot-info">
<strong>${plotInfo.name}</strong><br>
<small>${plotInfo.path}</small>
</div>
`;
}
if (title) title.textContent = plotInfo.name;
if (replaceBtn) replaceBtn.style.display = 'block';
// Reset grid item styles
const gridItems = document.querySelectorAll('.grid-item');
gridItems.forEach(item => {
item.style.cursor = '';
item.style.border = '';
});
}
/**
* Replace plot in comparison
*/
replacePlot(slot) {
this.selectPlotForComparison(slot);
}
}
+68
View File
@@ -0,0 +1,68 @@
/**
* Main Gallery Application
* Orchestrates all the different managers and functionality
*/
import { ThemeManager } from './theme-manager.js';
import { NavigationManager } from './navigation-manager.js';
import { SearchManager } from './search-manager.js';
import { RecentPlotsManager } from './recent-plots-manager.js';
import { ComparisonManager } from './comparison-manager.js';
import { StatsManager } from './stats-manager.js';
import { KeyboardManager } from './keyboard-manager.js';
import { Utils } from './utils.js';
/**
* Main Gallery Application Class
*/
export class GalleryApp {
constructor(config = {}) {
// Configuration from backend template variables
this.SEARCH_DEBOUNCE_MS = config.searchDebounceMs || 300;
this.MAX_RECENT_PLOTS = config.maxRecentPlots || 20;
this.stats = config.stats || null;
// Initialize managers
this.themeManager = new ThemeManager();
this.navigationManager = new NavigationManager();
this.searchManager = new SearchManager(this.SEARCH_DEBOUNCE_MS);
this.recentPlotsManager = new RecentPlotsManager(this.MAX_RECENT_PLOTS);
this.comparisonManager = new ComparisonManager();
this.statsManager = new StatsManager();
this.keyboardManager = new KeyboardManager(this);
this.utils = Utils;
// Set global references for backward compatibility
window.themeManager = this.themeManager;
window.searchManager = this.searchManager;
window.recentPlotsManager = this.recentPlotsManager;
window.comparisonManager = this.comparisonManager;
window.utils = this.utils;
this.init();
}
/**
* Initialize the gallery application
*/
init() {
this.navigationManager.buildBreadcrumb();
this.navigationManager.buildFolderTree();
// Update stats with backend data if available
if (this.stats) {
this.statsManager.updateWithBackendStats(this.stats);
}
// Handle URL-based thumbnail highlighting
Utils.handleThumbnailHighlight();
}
// 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); }
replacePlot(slot) { this.comparisonManager.replacePlot(slot); }
}
+109
View File
@@ -0,0 +1,109 @@
/**
* Keyboard shortcuts management
*/
export class KeyboardManager {
constructor(galleryApp) {
this.app = galleryApp;
this.init();
}
/**
* Initialize keyboard shortcuts
*/
init() {
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'k') {
e.preventDefault();
const searchBox = document.getElementById('searchBox');
if (searchBox) searchBox.focus();
}
if (e.ctrlKey && e.key === 'r') {
e.preventDefault();
if (this.app.recentPlotsManager) {
this.app.recentPlotsManager.toggleSidebar();
}
}
if (e.ctrlKey && e.key === 'c') {
e.preventDefault();
if (this.app.comparisonManager) {
this.app.comparisonManager.toggleCompareMode();
}
}
if (e.ctrlKey && e.key === 't') {
e.preventDefault();
if (this.app.themeManager) {
this.app.themeManager.toggle();
}
}
if (e.key === 'F5') {
e.preventDefault();
if (this.app.utils && this.app.utils.refreshGallery) {
this.app.utils.refreshGallery();
}
}
if (e.key === '?' && !e.ctrlKey && !e.altKey && !e.metaKey) {
e.preventDefault();
if (this.app.utils && this.app.utils.toggleShortcutsHelp) {
this.app.utils.toggleShortcutsHelp();
}
}
if (e.key === 'Escape') {
this.handleEscape();
}
});
}
/**
* Handle escape key actions
*/
handleEscape() {
// If in plot selection mode, cancel it
if (this.app.comparisonManager && this.app.comparisonManager.comparisonSlot) {
// Find and remove instruction element
const instruction = document.getElementById('comparisonInstruction');
if (instruction) instruction.remove();
// Reset grid item styles
const gridItems = document.querySelectorAll('.grid-item');
gridItems.forEach(item => {
item.style.cursor = '';
item.style.border = '';
});
// Show overlay again
const comparisonOverlay = document.getElementById('comparisonOverlay');
if (comparisonOverlay) comparisonOverlay.classList.add('open');
this.app.comparisonManager.comparisonSlot = null;
return;
}
// Close comparison overlay if open
const comparisonOverlay = document.getElementById('comparisonOverlay');
if (comparisonOverlay && comparisonOverlay.classList.contains('open')) {
if (this.app.comparisonManager) {
this.app.comparisonManager.hideComparisonOverlay();
}
return;
}
// Other ESC behaviors
const searchResults = document.getElementById('searchResults');
if (searchResults) searchResults.style.display = 'none';
const sidebar = document.getElementById('sidebar');
if (sidebar && sidebar.classList.contains('open')) {
if (this.app.recentPlotsManager) {
this.app.recentPlotsManager.toggleSidebar();
}
}
const shortcutsHelp = document.getElementById('shortcutsHelp');
if (shortcutsHelp) shortcutsHelp.style.display = 'none';
}
}
+29
View File
@@ -0,0 +1,29 @@
/**
* Main entry point for the Gallery application
* This file initializes the app when the DOM is ready
*/
import { GalleryApp } from './gallery-app.js';
// Global app instance for backward compatibility
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() {
// Configuration will be injected by the template
const config = window.galleryConfig || {};
app = new GalleryApp(config);
// Make app globally available
window.app = app;
});
+208
View File
@@ -0,0 +1,208 @@
/**
* Navigation functionality - breadcrumbs and folder tree
*/
export class NavigationManager {
/**
* Build breadcrumb navigation based on current path
*/
buildBreadcrumb() {
const currentPath = window.location.pathname;
const pathParts = currentPath.split('/').filter(part => part !== '' && part !== 'index.html');
const breadcrumb = document.getElementById('breadcrumb');
if (!breadcrumb) return;
if (pathParts.length === 0) {
breadcrumb.innerHTML = '<span>🏠 Root</span>';
return;
}
let html = '<a href="/">🏠 Root</a>';
for (let i = 0; i < pathParts.length; i++) {
const part = pathParts[i];
html += '<span class="separator">/</span>';
if (i === pathParts.length - 1) {
html += `<span>${decodeURIComponent(part)}</span>`;
} else {
const levelsUp = pathParts.length - 1 - i;
const relativePath = '../'.repeat(levelsUp) + 'index.html';
html += `<a href="${relativePath}">${decodeURIComponent(part)}</a>`;
}
}
breadcrumb.innerHTML = html;
}
/**
* Build and display the folder tree structure
*/
async buildFolderTree() {
const treeContainer = document.getElementById('folderTree');
const currentPath = window.location.pathname;
if (!treeContainer) return;
try {
const tree = await this.buildTreeRecursive(currentPath, 0, currentPath);
treeContainer.innerHTML = tree;
} catch (error) {
console.error('Error building folder tree:', error);
treeContainer.innerHTML = '<div class="tree-item">❌ Error loading folder tree</div>';
}
}
/**
* Recursively build tree structure for folders with collapsed empty directories
*/
async buildTreeRecursive(path, depth, currentPath, maxDepth = 5) {
if (depth > maxDepth) {
const indent = ' '.repeat(depth);
return `<div class="tree-item">${indent}└─ ...</div>`;
}
try {
const response = await fetch(path);
const htmlContent = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');
const subdirs = doc.querySelectorAll('h2 + ul li a');
const items = doc.querySelectorAll('.grid-item');
// Check if this is an empty directory (only one subdirectory, no items)
if (items.length === 0 && subdirs.length === 1) {
const subdir = subdirs[0];
const href = subdir.getAttribute('href');
if (href) {
const baseUrl = path.replace(/\/[^\/]*$/, '/');
const subPath = baseUrl + href;
const collapsedPath = await this.getCollapsedPath(path, subPath);
return await this.buildCollapsedTreeItem(collapsedPath, depth, currentPath, maxDepth);
}
}
// Normal directory processing
const indent = ' '.repeat(depth);
const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery';
const totalItems = items.length + subdirs.length;
const arrow = depth === 0 ? '' : '└─ ';
let html = '';
if (path === currentPath) {
html += `<div class="tree-item">${indent}${arrow}📁 <span class="tree-current">${folderName}</span> (${totalItems} items)</div>`;
} else {
html += `<div class="tree-item">${indent}${arrow}📁 <a href="${path}" class="tree-link">${folderName}</a> (${totalItems} items)</div>`;
}
for (let i = 0; i < subdirs.length; i++) {
const subdir = subdirs[i];
const href = subdir.getAttribute('href');
if (href) {
const baseUrl = path.replace(/\/[^\/]*$/, '/');
const subPath = baseUrl + href;
html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth);
}
}
return html;
} catch (error) {
const indent = ' '.repeat(depth);
const arrow = depth === 0 ? '' : '└─ ';
const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery';
return `<div class="tree-item">${indent}${arrow}📁 ${folderName} (error loading)</div>`;
}
}
/**
* Get the collapsed path by following empty directories
*/
async getCollapsedPath(startPath, currentPath) {
const pathSegments = [];
let path = startPath;
while (true) {
try {
const response = await fetch(path);
const htmlContent = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');
const subdirs = doc.querySelectorAll('h2 + ul li a');
const items = doc.querySelectorAll('.grid-item');
const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery';
pathSegments.push({ name: folderName, path: path });
if (items.length > 0 || subdirs.length !== 1) {
break;
}
const subdir = subdirs[0];
const href = subdir.getAttribute('href');
if (href) {
const baseUrl = path.replace(/\/[^\/]*$/, '/');
path = baseUrl + href;
} else {
break;
}
} catch (error) {
break;
}
}
return {
segments: pathSegments,
finalPath: path
};
}
/**
* Build a collapsed tree item for empty directory chains
*/
async buildCollapsedTreeItem(collapsedPath, depth, currentPath, maxDepth) {
const indent = ' '.repeat(depth);
const arrow = depth === 0 ? '' : '└─ ';
const displayName = collapsedPath.segments.map(seg => seg.name).join(' / ');
const finalPath = collapsedPath.finalPath;
let totalItems = 0;
let subdirs = [];
try {
const response = await fetch(finalPath);
const htmlContent = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');
const subdirElements = doc.querySelectorAll('h2 + ul li a');
const items = doc.querySelectorAll('.grid-item');
totalItems = items.length + subdirElements.length;
subdirs = Array.from(subdirElements);
} catch (error) {
// Handle error case
}
let html = '';
if (finalPath === currentPath) {
html += `<div class="tree-item">${indent}${arrow}📁 <span class="tree-current">${displayName}</span> (${totalItems} items)</div>`;
} else {
html += `<div class="tree-item">${indent}${arrow}📁 <a href="${finalPath}" class="tree-link">${displayName}</a> (${totalItems} items)</div>`;
}
for (let i = 0; i < subdirs.length; i++) {
const subdir = subdirs[i];
const href = subdir.getAttribute('href');
if (href) {
const baseUrl = finalPath.replace(/\/[^\/]*$/, '/');
const subPath = baseUrl + href;
html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth);
}
}
return html;
}
}
+114
View File
@@ -0,0 +1,114 @@
/**
* Recent plots sidebar management
*/
export class RecentPlotsManager {
constructor(maxRecentPlots = 20) {
this.MAX_RECENT_PLOTS = maxRecentPlots;
this.init();
}
init() {
this.updateRecentPlotsDisplay();
this.trackPlotClicks();
}
/**
* Add plot to recent plots list
*/
addToRecentPlots(plotHref) {
const plotName = plotHref.split('/').pop().replace('.pdf', '');
const pathParts = plotHref.split('/').filter(p => p !== '' && p !== plotName + '.pdf');
const plotPath = pathParts.join(' / ');
const thumbUrl = plotHref.replace('.pdf', '.png');
// Determine the gallery page URL (directory containing the plot)
const plotDir = plotHref.substring(0, plotHref.lastIndexOf('/'));
const galleryUrl = plotDir + '/index.html';
const plotInfo = {
name: plotName,
path: plotPath,
href: plotHref,
thumbUrl: thumbUrl,
galleryUrl: galleryUrl,
timestamp: Date.now()
};
let recentPlots = JSON.parse(localStorage.getItem('recentPlots') || '[]');
recentPlots = recentPlots.filter(p => p.href !== plotHref);
recentPlots.unshift(plotInfo);
recentPlots = recentPlots.slice(0, this.MAX_RECENT_PLOTS);
localStorage.setItem('recentPlots', JSON.stringify(recentPlots));
this.updateRecentPlotsDisplay();
}
/**
* Update recent plots sidebar display
*/
updateRecentPlotsDisplay() {
const sidebarContent = document.getElementById('sidebarContent');
if (!sidebarContent) return;
const recentPlots = JSON.parse(localStorage.getItem('recentPlots') || '[]');
if (recentPlots.length === 0) {
sidebarContent.innerHTML = `
<div style="text-align: center; color: var(--breadcrumb-color); margin: 2rem 0;">
📭 No recent plots yet<br>
<small style="opacity: 0.7;">Open some plots to see them here</small>
</div>
`;
return;
}
let html = '';
recentPlots.forEach(plot => {
html += `
<div class="recent-plot" onclick="window.recentPlotsManager.openRecentPlot('${plot.galleryUrl || plot.href}', '${plot.name}')" title="${plot.name}">
<img src="${plot.thumbUrl}" class="recent-plot-thumb" alt="${plot.name}" />
<div class="recent-plot-info">
<div class="recent-plot-name">${plot.name}</div>
<div class="recent-plot-path">📍 ${plot.path}</div>
</div>
</div>
`;
});
sidebarContent.innerHTML = html;
}
/**
* Open recent plot gallery page and highlight thumbnail
*/
openRecentPlot(galleryUrl, plotName) {
// Navigate to gallery page with plot highlight parameter
const url = new URL(galleryUrl, window.location.origin);
url.searchParams.set('highlight', plotName);
window.location.href = url.toString();
this.toggleSidebar();
}
/**
* Toggle recent plots sidebar
*/
toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
if (sidebar) sidebar.classList.toggle('open');
if (overlay) overlay.classList.toggle('open');
}
/**
* Track clicks on plot links
*/
trackPlotClicks() {
document.addEventListener('click', (e) => {
const link = e.target.closest('a[href$=".pdf"]');
if (link) {
this.addToRecentPlots(link.href);
}
});
}
}
+217
View File
@@ -0,0 +1,217 @@
/**
* Search functionality
*/
export class SearchManager {
constructor(debounceMs = 300) {
this.searchTimeout = null;
this.SEARCH_DEBOUNCE_MS = debounceMs;
this.init();
}
/**
* Initialize search functionality with debouncing
*/
init() {
const searchBox = document.getElementById('searchBox');
const searchResults = document.getElementById('searchResults');
if (!searchBox || !searchResults) return;
searchBox.addEventListener('input', (e) => {
clearTimeout(this.searchTimeout);
const query = e.target.value.trim();
if (query.length === 0) {
searchResults.style.display = 'none';
return;
}
this.searchTimeout = setTimeout(() => {
this.performSearch(query);
}, this.SEARCH_DEBOUNCE_MS);
});
document.addEventListener('click', (e) => {
if (!searchBox.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.style.display = 'none';
}
});
}
/**
* Perform search across plot names
*/
async performSearch(query) {
const searchResults = document.getElementById('searchResults');
if (!searchResults) return;
searchResults.innerHTML = '<div style="padding: 1rem;">🔍 Searching...</div>';
searchResults.style.display = 'block';
try {
const results = await this.searchPlots(query);
this.displaySearchResults(results, query);
} catch (error) {
console.error('Search error:', error);
searchResults.innerHTML = '<div style="padding: 1rem; color: red;">❌ Search failed</div>';
}
}
/**
* Search for plots matching the query
*/
async searchPlots(query) {
const results = [];
const visited = new Set();
const lowerQuery = query.toLowerCase();
await this.searchInPage(window.location.pathname, lowerQuery, results, visited);
await this.searchRecursive(window.location.pathname, lowerQuery, results, visited, 0, 5);
return results.slice(0, 20);
}
/**
* Search for plots in a specific page
*/
async searchInPage(path, query, results, visited, maxResults = 50) {
if (visited.has(path) || results.length >= maxResults) return;
visited.add(path);
try {
const response = await fetch(path);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const items = doc.querySelectorAll('.grid-item');
items.forEach(item => {
const nameElement = item.querySelector('.plot-name');
const imgElement = item.querySelector('img');
const linkElement = item.querySelector('a');
if (nameElement && imgElement && linkElement) {
const name = nameElement.textContent.toLowerCase();
if (name.includes(query)) {
results.push({
name: nameElement.textContent,
path: path,
href: linkElement.href,
imgSrc: imgElement.src,
relevance: this.calculateRelevance(name, query)
});
}
}
});
} catch (error) {
console.error('Error searching in', path, error);
}
}
/**
* Recursively search in subdirectories
*/
async searchRecursive(path, query, results, visited, depth, maxDepth) {
if (depth >= maxDepth || results.length >= 50) return;
try {
const response = await fetch(path);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const subdirs = doc.querySelectorAll('h2 + ul li a');
for (const subdir of subdirs) {
const href = subdir.getAttribute('href');
if (href) {
const baseUrl = path.replace(/\/[^\/]*$/, '/');
const subPath = baseUrl + href;
await this.searchInPage(subPath, query, results, visited);
await this.searchRecursive(subPath, query, results, visited, depth + 1, maxDepth);
}
}
} catch (error) {
console.error('Error in recursive search:', error);
}
}
/**
* Calculate search relevance score
*/
calculateRelevance(text, query) {
const exactMatch = text === query;
const startsWith = text.startsWith(query);
const wordMatch = text.split(/\s+/).some(word => word.startsWith(query));
if (exactMatch) return 100;
if (startsWith) return 80;
if (wordMatch) return 60;
return 40;
}
/**
* Display search results with highlighting
*/
displaySearchResults(results, query) {
const searchResults = document.getElementById('searchResults');
if (!searchResults) return;
if (results.length === 0) {
searchResults.innerHTML = '<div style="padding: 1rem;">📭 No plots found</div>';
return;
}
results.sort((a, b) => b.relevance - a.relevance);
let html = '';
results.forEach(result => {
const highlightedName = this.highlightText(result.name, query);
const relativePath = this.getRelativePath(result.path);
html += `
<div class="search-result-item" onclick="window.searchManager.openSearchResult('${result.href}')" title="${result.name}">
<div style="display: flex; gap: 0.7rem; align-items: center;">
<img src="${result.imgSrc}" style="width: 50px; height: 40px; object-fit: cover; border-radius: 4px;" />
<div style="flex: 1; min-width: 0;">
<div style="font-weight: 600; margin-bottom: 0.3rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
${highlightedName}
</div>
<div style="font-size: 0.8rem; color: var(--breadcrumb-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
📍 ${relativePath}
</div>
</div>
</div>
</div>
`;
});
searchResults.innerHTML = html;
}
/**
* Highlight search query in text
*/
highlightText(text, query) {
const regex = new RegExp(`(${query})`, 'gi');
return text.replace(regex, '<span class="search-highlight">$1</span>');
}
/**
* Get relative path for display
*/
getRelativePath(fullPath) {
const parts = fullPath.split('/').filter(p => p !== '' && p !== 'index.html');
return parts.length > 0 ? parts.join(' / ') : 'Root';
}
/**
* Open search result and track it
*/
openSearchResult(href) {
if (window.recentPlotsManager) {
window.recentPlotsManager.addToRecentPlots(href);
}
window.open(href, '_blank');
document.getElementById('searchResults').style.display = 'none';
}
}
+54
View File
@@ -0,0 +1,54 @@
/**
* Statistics manager for gallery display
*/
export class StatsManager {
constructor() {
this.updateGalleryStats();
}
/**
* Update gallery statistics display
*/
updateGalleryStats() {
// Use stats passed from Python backend if available
// Otherwise fallback to DOM counting
const gridItems = document.querySelectorAll('.grid-item');
const subdirLinks = document.querySelectorAll('a[href$="/index.html"]');
const fileCountEl = document.getElementById('fileCount');
const folderCountEl = document.getElementById('folderCount');
const totalSizeEl = document.getElementById('totalSize');
const lastUpdatedEl = document.getElementById('lastUpdated');
if (fileCountEl) fileCountEl.textContent = gridItems.length;
if (folderCountEl) folderCountEl.textContent = subdirLinks.length;
if (totalSizeEl) totalSizeEl.textContent = 'Unknown';
// Set last updated time
const now = new Date();
const timeStr = now.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
if (lastUpdatedEl) lastUpdatedEl.textContent = timeStr;
}
/**
* Update stats with backend data
*/
updateWithBackendStats(stats) {
const fileCountEl = document.getElementById('fileCount');
const folderCountEl = document.getElementById('folderCount');
const totalSizeEl = document.getElementById('totalSize');
if (fileCountEl && stats.file_count !== undefined) {
fileCountEl.textContent = stats.file_count;
}
if (folderCountEl && stats.folder_count !== undefined) {
folderCountEl.textContent = stats.folder_count;
}
if (totalSizeEl && stats.total_size !== undefined) {
totalSizeEl.textContent = stats.total_size;
}
}
}
+43
View File
@@ -0,0 +1,43 @@
/**
* Theme management functionality
*/
export class ThemeManager {
constructor() {
this.init();
}
/**
* Initialize theme system and load saved preference
*/
init() {
const savedTheme = localStorage.getItem('theme');
const html = document.documentElement;
const themeToggle = document.getElementById('themeToggle');
if (savedTheme === 'light') {
html.setAttribute('data-theme', 'light');
if (themeToggle) themeToggle.textContent = '🌙';
} else {
html.removeAttribute('data-theme');
if (themeToggle) themeToggle.textContent = '☀️';
}
}
/**
* Toggle between light and dark themes
*/
toggle() {
const html = document.documentElement;
const themeToggle = document.getElementById('themeToggle');
if (html.getAttribute('data-theme') === 'light') {
html.removeAttribute('data-theme');
if (themeToggle) themeToggle.textContent = '☀️';
localStorage.setItem('theme', 'dark');
} else {
html.setAttribute('data-theme', 'light');
if (themeToggle) themeToggle.textContent = '🌙';
localStorage.setItem('theme', 'light');
}
}
}
+143
View File
@@ -0,0 +1,143 @@
/**
* Utility functions and helpers
*/
export class Utils {
/**
* Format file size in human readable format
*/
static formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
/**
* Handle thumbnail highlighting from URL parameters
*/
static handleThumbnailHighlight() {
const urlParams = new URLSearchParams(window.location.search);
const highlightPlot = urlParams.get('highlight');
if (highlightPlot) {
// Find and highlight the thumbnail
const gridItems = document.querySelectorAll('.grid-item');
gridItems.forEach(item => {
const plotName = item.querySelector('.plot-name');
if (plotName && plotName.textContent.trim() === highlightPlot) {
item.classList.add('highlighted');
// Scroll to the highlighted item
setTimeout(() => {
item.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}, 100);
// Remove highlight after animation
setTimeout(() => {
item.classList.remove('highlighted');
}, 3000);
}
});
// Clean up URL
const newUrl = new URL(window.location);
newUrl.searchParams.delete('highlight');
window.history.replaceState({}, document.title, newUrl.toString());
}
}
/**
* Calculate approximate total size of displayed files
*/
static async calculateApproximateSize() {
const images = document.querySelectorAll('.grid-item img');
let totalSize = 0;
let loadedCount = 0;
const sizeElement = document.getElementById('totalSize');
if (!sizeElement) return;
sizeElement.textContent = 'Loading...';
// Estimate size based on a sample of images
const sampleSize = Math.min(images.length, 5);
const sampleImages = Array.from(images).slice(0, sampleSize);
if (sampleImages.length === 0) {
sizeElement.textContent = '0 KB';
return;
}
// Calculate average size from sample
for (const img of sampleImages) {
try {
const response = await fetch(img.src, { method: 'HEAD' });
const size = parseInt(response.headers.get('content-length') || '0');
if (size > 0) {
totalSize += size;
loadedCount++;
}
} catch (e) {
// Fallback: estimate 100KB per image
totalSize += 102400;
loadedCount++;
}
}
if (loadedCount > 0) {
const averageSize = totalSize / loadedCount;
const estimatedTotal = averageSize * images.length;
sizeElement.textContent = Utils.formatFileSize(estimatedTotal);
} else {
sizeElement.textContent = 'Unknown';
}
}
/**
* Refresh gallery by calling CGI script
*/
static async refreshGallery() {
const refreshBtn = document.getElementById('refreshBtn');
if (!refreshBtn) return;
refreshBtn.disabled = true;
refreshBtn.textContent = '⏳';
try {
const response = await fetch('/cgi-bin/refresh_gallery.py', {
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
*/
static toggleShortcutsHelp() {
const help = document.getElementById('shortcutsHelp');
if (help) {
help.style.display = help.style.display === 'block' ? 'none' : 'block';
}
}
}
+15 -8
View File
@@ -1756,18 +1756,23 @@
});
// Show overlay again
document.getElementById('comparisonOverlay').style.display = 'flex';
document.getElementById('comparisonOverlay').classList.add('open');
this.comparisonSlot = null;
return;
}
// Close comparison overlay if open
const comparisonOverlay = document.getElementById('comparisonOverlay');
if (comparisonOverlay && comparisonOverlay.classList.contains('open')) {
this.hideComparisonOverlay();
return;
}
// Other ESC behaviors
document.getElementById('searchResults').style.display = 'none';
if (document.getElementById('sidebar').classList.contains('open')) {
this.toggleSidebar();
}
if (document.getElementById('comparisonOverlay').classList.contains('open')) {
this.hideComparisonOverlay();
}
document.getElementById('shortcutsHelp').style.display = 'none';
}
});
@@ -1867,9 +1872,9 @@
selectPlotForComparison(slot) {
this.comparisonSlot = slot;
// Hide overlay temporarily
// Hide overlay temporarily by removing the 'open' class
const overlay = document.getElementById('comparisonOverlay');
overlay.style.display = 'none';
overlay.classList.remove('open');
// Show simple alert with instructions
const instruction = document.createElement('div');
@@ -1915,7 +1920,8 @@
// Clean up
instruction.remove();
gridItems.forEach(item => item.removeEventListener('click', handleClick));
overlay.style.display = 'flex';
this.comparisonSlot = null;
overlay.classList.add('open');
};
gridItems.forEach(item => {
@@ -1933,7 +1939,8 @@
item.style.cursor = '';
item.style.border = '';
});
overlay.style.display = 'flex';
this.comparisonSlot = null;
overlay.classList.add('open');
document.removeEventListener('keydown', handleEscape);
}
};
+189
View File
@@ -0,0 +1,189 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
<link rel="stylesheet" href="assets/css/main.css">
</head>
<body>
<!-- Main Content -->
<h1>{{ title }}</h1>
<!-- Search Bar -->
<div class="search-container">
<input type="text" class="search-box" id="searchBox" placeholder="Search plots..." />
<span class="search-icon">🔍</span>
<div class="search-results" id="searchResults"></div>
</div>
<!-- Breadcrumb Navigation -->
<div class="breadcrumb" id="breadcrumb"></div>
<!-- Navigation Buttons -->
<div class="navigation">
<button class="nav-btn" onclick="window.history.back()">
← Back
</button>
{% if relpath != "." %}
<a href="../index.html" class="nav-btn">
↑ Parent Directory
</a>
{% endif %}
</div>
<!-- Folder Tree -->
<div class="folder-tree" id="folderTree"></div>
<!-- Plot Grid -->
<div class="grid">
{% for item in items %}
<div class="grid-item">
<a href="{{ item.pdf_href }}">
<img src="{{ item.png_href }}" alt="{{ item.name }}">
</a>
<div class="plot-name" title="{{ item.name }}">{{ item.name }}</div>
</div>
{% endfor %}
</div>
<!-- Subdirectories -->
{% if subdirs %}
<h2>Subdirectories</h2>
<ul>
{% for sub in subdirs %}
<li><a href="{{ sub }}/index.html">📁 {{ sub }}</a></li>
{% endfor %}
</ul>
{% endif %}
<!-- Recent Plots Sidebar -->
<div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleSidebar()"></div>
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<h3 class="sidebar-title">Recent Plots</h3>
<button class="sidebar-close" onclick="toggleSidebar()">×</button>
</div>
<div class="sidebar-content" id="sidebarContent">
<div style="text-align: center; color: var(--breadcrumb-color); margin: 2rem 0;">
No recent plots yet
</div>
</div>
</div>
<!-- Floating Action Buttons -->
<div class="floating-buttons">
<button class="floating-btn sidebar-toggle" onclick="toggleSidebar()" id="sidebarToggle" title="Recent Plots (Ctrl+R)">
📋
</button>
<button class="floating-btn compare-toggle" onclick="app.toggleCompareMode()" id="compareToggle" title="Compare Plots (Ctrl+C)">
⚖️
</button>
<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 -->
<div class="shortcuts-help" id="shortcutsHelp">
<h4>Keyboard Shortcuts</h4>
<div class="shortcut-item">
<span>Search</span>
<span class="shortcut-key">Ctrl+K</span>
</div>
<div class="shortcut-item">
<span>Recent plots</span>
<span class="shortcut-key">Ctrl+R</span>
</div>
<div class="shortcut-item">
<span>Compare plots</span>
<span class="shortcut-key">Ctrl+C</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>
</div>
<div class="shortcut-item">
<span>Help</span>
<span class="shortcut-key">?</span>
</div>
</div>
<!-- Gallery Statistics -->
<div class="gallery-stats" id="galleryStats">
<div class="stats-item">
<span class="stats-label">📊 Files:</span>
<span class="stats-value" id="fileCount">0</span>
</div>
<div class="stats-item">
<span class="stats-label">📁 Folders:</span>
<span class="stats-value" id="folderCount">0</span>
</div>
<div class="stats-item">
<span class="stats-label">💾 Size:</span>
<span class="stats-value" id="totalSize">0 KB</span>
</div>
<div class="stats-item">
<span class="stats-label">🕒 Updated:</span>
<span class="stats-value" id="lastUpdated">Now</span>
</div>
</div>
<!-- Plot Comparison Overlay -->
<div class="comparison-overlay" id="comparisonOverlay">
<div class="comparison-container">
<div class="comparison-header">
<h2 class="comparison-title">Plot Comparison</h2>
<button class="comparison-close" onclick="app.closeComparison()" title="Close Comparison (Esc)">×</button>
</div>
<div class="comparison-content">
<div class="comparison-panel">
<div class="comparison-panel-header">
<span class="comparison-panel-title" id="leftPlotTitle">Plot A</span>
<button class="comparison-replace-btn" onclick="app.replacePlot('left')" id="leftReplaceBtn">Replace</button>
</div>
<div class="comparison-panel-content">
<div class="comparison-plot-container" id="leftPlotContainer">
<div class="comparison-placeholder" onclick="app.selectPlotForComparison('left')">
📊 Click to select first plot
</div>
</div>
</div>
</div>
<div class="comparison-panel">
<div class="comparison-panel-header">
<span class="comparison-panel-title" id="rightPlotTitle">Plot B</span>
<button class="comparison-replace-btn" onclick="app.replacePlot('right')" id="rightReplaceBtn">Replace</button>
</div>
<div class="comparison-panel-content">
<div class="comparison-plot-container" id="rightPlotContainer">
<div class="comparison-placeholder" onclick="app.selectPlotForComparison('right')">
📊 Click to select second plot
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Configuration for JavaScript -->
<script>
// Configuration object for the gallery app
window.galleryConfig = {
searchDebounceMs: {{ ui.search_debounce_ms|default(300) }},
maxRecentPlots: {{ ui.max_recent_plots|default(20) }},
stats: {% if stats %}{{ stats|tojson }}{% else %}null{% endif %}
};
</script>
<!-- Main JavaScript Application -->
<script type="module" src="assets/js/main.js"></script>
</body>
</html>