Move jv and css to own folder
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user