diff --git a/README.md b/README.md index f1b6159..3b4f21b 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file +## 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. diff --git a/assets/css/base.css b/assets/css/base.css new file mode 100644 index 0000000..ae700dc --- /dev/null +++ b/assets/css/base.css @@ -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; +} diff --git a/assets/css/comparison.css b/assets/css/comparison.css new file mode 100644 index 0000000..7e2dee2 --- /dev/null +++ b/assets/css/comparison.css @@ -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; +} diff --git a/assets/css/floating-elements.css b/assets/css/floating-elements.css new file mode 100644 index 0000000..1c5bb2f --- /dev/null +++ b/assets/css/floating-elements.css @@ -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; +} diff --git a/assets/css/folder-tree.css b/assets/css/folder-tree.css new file mode 100644 index 0000000..3c394ad --- /dev/null +++ b/assets/css/folder-tree.css @@ -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); +} diff --git a/assets/css/grid.css b/assets/css/grid.css new file mode 100644 index 0000000..5f633a2 --- /dev/null +++ b/assets/css/grid.css @@ -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; +} diff --git a/assets/css/main.css b/assets/css/main.css new file mode 100644 index 0000000..8162397 --- /dev/null +++ b/assets/css/main.css @@ -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'); diff --git a/assets/css/navigation.css b/assets/css/navigation.css new file mode 100644 index 0000000..1dc5629 --- /dev/null +++ b/assets/css/navigation.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; +} diff --git a/assets/css/responsive.css b/assets/css/responsive.css new file mode 100644 index 0000000..7b2a7a8 --- /dev/null +++ b/assets/css/responsive.css @@ -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%; + } +} diff --git a/assets/css/search.css b/assets/css/search.css new file mode 100644 index 0000000..7bd6995 --- /dev/null +++ b/assets/css/search.css @@ -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; +} diff --git a/assets/css/sidebar.css b/assets/css/sidebar.css new file mode 100644 index 0000000..7718fb1 --- /dev/null +++ b/assets/css/sidebar.css @@ -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; +} diff --git a/assets/css/stats.css b/assets/css/stats.css new file mode 100644 index 0000000..cec72dc --- /dev/null +++ b/assets/css/stats.css @@ -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); +} diff --git a/assets/css/variables.css b/assets/css/variables.css new file mode 100644 index 0000000..72c531e --- /dev/null +++ b/assets/css/variables.css @@ -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; +} diff --git a/assets/js/comparison-manager.js b/assets/js/comparison-manager.js new file mode 100644 index 0000000..7056775 --- /dev/null +++ b/assets/js/comparison-manager.js @@ -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 = ` + ${plotInfo.name} +
+ ${plotInfo.name}
+ ${plotInfo.path} +
+ `; + } + + 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); + } +} diff --git a/assets/js/gallery-app.js b/assets/js/gallery-app.js new file mode 100644 index 0000000..01eb39e --- /dev/null +++ b/assets/js/gallery-app.js @@ -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); } +} diff --git a/assets/js/keyboard-manager.js b/assets/js/keyboard-manager.js new file mode 100644 index 0000000..5936d62 --- /dev/null +++ b/assets/js/keyboard-manager.js @@ -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'; + } +} diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 0000000..adcfd02 --- /dev/null +++ b/assets/js/main.js @@ -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; +}); diff --git a/assets/js/navigation-manager.js b/assets/js/navigation-manager.js new file mode 100644 index 0000000..b9a69d6 --- /dev/null +++ b/assets/js/navigation-manager.js @@ -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 = '🏠 Root'; + return; + } + + let html = '🏠 Root'; + + for (let i = 0; i < pathParts.length; i++) { + const part = pathParts[i]; + html += '/'; + + if (i === pathParts.length - 1) { + html += `${decodeURIComponent(part)}`; + } else { + const levelsUp = pathParts.length - 1 - i; + const relativePath = '../'.repeat(levelsUp) + 'index.html'; + html += `${decodeURIComponent(part)}`; + } + } + + 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 = '
❌ Error loading folder tree
'; + } + } + + /** + * 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 `
${indent}└─ ...
`; + } + + 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 += `
${indent}${arrow}📁 ${folderName} (${totalItems} items)
`; + } else { + html += `
${indent}${arrow}📁 ${folderName} (${totalItems} items)
`; + } + + 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 `
${indent}${arrow}📁 ${folderName} (error loading)
`; + } + } + + /** + * 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 += `
${indent}${arrow}📁 ${displayName} (${totalItems} items)
`; + } else { + html += `
${indent}${arrow}📁 ${displayName} (${totalItems} items)
`; + } + + 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; + } +} diff --git a/assets/js/recent-plots-manager.js b/assets/js/recent-plots-manager.js new file mode 100644 index 0000000..29fd3bc --- /dev/null +++ b/assets/js/recent-plots-manager.js @@ -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 = ` +
+ 📭 No recent plots yet
+ Open some plots to see them here +
+ `; + return; + } + + let html = ''; + recentPlots.forEach(plot => { + html += ` +
+ ${plot.name} +
+
${plot.name}
+
📍 ${plot.path}
+
+
+ `; + }); + + 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); + } + }); + } +} diff --git a/assets/js/search-manager.js b/assets/js/search-manager.js new file mode 100644 index 0000000..20eff08 --- /dev/null +++ b/assets/js/search-manager.js @@ -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 = '
🔍 Searching...
'; + searchResults.style.display = 'block'; + + try { + const results = await this.searchPlots(query); + this.displaySearchResults(results, query); + } catch (error) { + console.error('Search error:', error); + searchResults.innerHTML = '
❌ Search failed
'; + } + } + + /** + * 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 = '
📭 No plots found
'; + 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 += ` +
+
+ +
+
+ ${highlightedName} +
+
+ 📍 ${relativePath} +
+
+
+
+ `; + }); + + searchResults.innerHTML = html; + } + + /** + * Highlight search query in text + */ + highlightText(text, query) { + const regex = new RegExp(`(${query})`, 'gi'); + return text.replace(regex, '$1'); + } + + /** + * 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'; + } +} diff --git a/assets/js/stats-manager.js b/assets/js/stats-manager.js new file mode 100644 index 0000000..9ad8df5 --- /dev/null +++ b/assets/js/stats-manager.js @@ -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; + } + } +} diff --git a/assets/js/theme-manager.js b/assets/js/theme-manager.js new file mode 100644 index 0000000..f23affc --- /dev/null +++ b/assets/js/theme-manager.js @@ -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'); + } + } +} diff --git a/assets/js/utils.js b/assets/js/utils.js new file mode 100644 index 0000000..78fb7e5 --- /dev/null +++ b/assets/js/utils.js @@ -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'; + } + } +} diff --git a/template.html b/template.html index 93f30e5..75a9ef5 100644 --- a/template.html +++ b/template.html @@ -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); } }; diff --git a/templates/gallery.html b/templates/gallery.html new file mode 100644 index 0000000..7e9bf17 --- /dev/null +++ b/templates/gallery.html @@ -0,0 +1,189 @@ + + + + + {{ title }} + + + + +

{{ title }}

+ + +
+ + 🔍 +
+
+ + + + + + + + +
+ + +
+ {% for item in items %} +
+ + {{ item.name }} + +
{{ item.name }}
+
+ {% endfor %} +
+ + + {% if subdirs %} +

Subdirectories

+ + {% endif %} + + + + + + +
+ + + + +
+ + +
+

Keyboard Shortcuts

+
+ Search + Ctrl+K +
+
+ Recent plots + Ctrl+R +
+
+ Compare plots + Ctrl+C +
+
+ Refresh + F5 +
+
+ Toggle theme + Ctrl+T +
+
+ Help + ? +
+
+ + + + + +
+
+
+

Plot Comparison

+ +
+
+
+
+ Plot A + +
+
+
+
+ 📊 Click to select first plot +
+
+
+
+
+
+ Plot B + +
+
+
+
+ 📊 Click to select second plot +
+
+
+
+
+
+
+ + + + + + + +