Refactor: Remove obsolete JavaScript managers and utilities

- Deleted NavigationManager, RecentPlotsManager, SearchManager, SortManager, StatsManager, ThemeManager, Utils, and ViewManager classes.
- Updated gallery builder to improve asset copying logic with a more efficient modification time check.
- Cleaned up gallery HTML template by removing subdirectory listing and embedding gallery data as JSON.
- Added CLAUDE.md for project documentation and guidelines.
This commit is contained in:
Kylian Schmidt
2026-05-06 08:34:40 +02:00
parent 6f0cf4521b
commit 6738c4ca69
44 changed files with 205 additions and 5239 deletions
+112
View File
@@ -0,0 +1,112 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What This Project Does
A Python package that generates responsive static HTML galleries from scientific plot collections (PDFs and HTMLs). It converts PDFs to PNGs via ImageMagick, organizes plots hierarchically, propagates YAML/JSON metadata through directory trees, and renders everything via a Jinja2 template into a static website served from a web directory.
## Commands
```bash
# Install the package (editable)
pip install -e ".[dev]"
# Run all tests
pytest tests/
# Run a single test file
pytest tests/test_generate_gallery.py -v
# Run a single test by name
pytest tests/test_generate_gallery.py::test_needs_update_missing_target -v
# Generate gallery (CLI entry point after install)
gallery --config config.yaml --verbose
# Generate gallery (legacy script)
python generate_gallery.py --verbose
# Incremental update for one source only
gallery --source /path/to/plots --verbose
# Clean regeneration
gallery --clean --verbose
# Serve output locally
python -m http.server 8000 -d /web/kschmidt/public_html/
```
Code style: black with `line-length = 120`.
## Architecture
### Execution Flow
```
generate() [api.py]
└── copy_assets() [builder.py] — copies assets/ to web output dir
└── get_template() [builder.py] — loads gallery/templates/gallery.html
└── build_gallery() [builder.py] — recursive per-source-directory walk
└── load_folder_metadata() [utils/metadata.py]
└── merge_metadata() [utils/metadata.py] — inherits from parent
└── process_plot_files() [utils/processing.py] — PDF→PNG, copy files
└── save_metadata_cache() [utils/metadata.py]
└── render_gallery_page() [utils/processing.py] — writes index.html
└── recurse into subdirs
```
### Key Files
| File | Role |
|------|------|
| `gallery/api.py` | `generate()` — primary public entry point; orchestrates everything |
| `gallery/builder.py` | `build_gallery()` — recursive traversal; `get_template()`, `copy_assets()` |
| `gallery/config.py` | `GalleryConfig`, `GallerySource`, `GalleryDefaults` dataclasses; YAML loading |
| `gallery/cli.py` | CLI (`gallery` command) wrapping `generate()` |
| `gallery/utils/processing.py` | PDF→PNG via ImageMagick subprocess; `needs_update()` timestamp check; `render_gallery_page()` |
| `gallery/utils/metadata.py` | Load/merge/cache YAML+JSON metadata; per-plot metadata resolution |
| `gallery/utils/stats.py` | Directory size/count statistics |
| `gallery/templates/gallery.html` | Single Jinja2 template for all gallery pages |
| `assets/js/` | Vanilla JS modules loaded as ES modules; `GalleryApp` in `gallery-app.js` orchestrates all managers |
| `assets/css/` | Modular CSS; `main.css` imports all others via `@import` |
| `generate_gallery.py` | Legacy CLI wrapper — kept for backward compatibility |
| `config.yaml` | Local deployment config (paths are machine-specific) |
### Config File Format
The YAML config uses a specific structure (not flat — must match `GalleryConfig.from_yaml()`):
```yaml
paths:
web_folder: "/web/user/public_html" # required
gallery:
plot_root: "gallery"
png_dpi: 400
sources:
- name: "my_plots"
path: "/path/to/plots"
metadata:
cache_enabled: true
inherit_from_parent: true
```
### Incremental Updates
`needs_update(source, target)` uses a **30-second buffer** on mtime comparisons to handle filesystem timing. This is intentional — avoid tightening it.
When `source_to_update` is passed to `generate()`, only that source's subdirectory is deleted and rebuilt; all other sources stay intact and the root index is re-rendered to include them.
### Metadata Inheritance
`metadata.yaml` (or `.yml`/`.json`) in any source directory is loaded and **merged with parent metadata** (`inherit_from_parent=True` by default). Child directories override parent keys. Per-plot overrides can live in `<plotname>.yaml` files alongside the plot.
### Frontend (Static JS/CSS)
The frontend is vanilla ES modules — no build step. `assets/js/main.js` imports `GalleryApp` from `gallery-app.js`, which instantiates all manager classes (`ThemeManager`, `SearchManager`, `NavigationManager`, etc.). Each manager is self-contained. The template embeds gallery data as JSON in the page; JS reads it at runtime.
Assets are served from `/assets/` relative to gallery pages. The Python code calculates the correct `../` depth per page when rendering the template.
### Deployment
The project ships a `Singularity.def` / `web.sif` Apptainer container for HPC environments. CI (`.gitlab-ci.yml`) builds the container and runs pytest inside it. For local development the `.venv` is sufficient.
+1 -8
View File
@@ -2,16 +2,9 @@
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;
margin: 1rem 0;
}
.tree-item {
+33 -47
View File
@@ -35,6 +35,22 @@ export class NavigationManager {
breadcrumb.innerHTML = html;
}
/**
* Extract subdirs and item count from a parsed page document.
* Reads from the embedded #gallery-data JSON element.
*/
extractPageData(doc) {
const dataEl = doc.getElementById('gallery-data');
if (dataEl) {
try {
const data = JSON.parse(dataEl.textContent);
return { subdirs: data.subdirs || [], itemCount: data.item_count || 0 };
} catch {}
}
// Fallback for pages that pre-date the data element
return { subdirs: [], itemCount: doc.querySelectorAll('.grid-item').length };
}
/**
* Build and display the folder tree structure
*/
@@ -68,26 +84,20 @@ export class NavigationManager {
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 { subdirs, itemCount } = this.extractPageData(doc);
const baseUrl = path.replace(/\/[^\/]*$/, '/');
// 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;
if (itemCount === 0 && subdirs.length === 1) {
const subPath = baseUrl + subdirs[0] + '/index.html';
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 totalItems = itemCount + subdirs.length;
const arrow = depth === 0 ? '' : '└─ ';
let html = '';
@@ -97,15 +107,10 @@ export class NavigationManager {
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;
for (const subdir of subdirs) {
const subPath = baseUrl + subdir + '/index.html';
html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth);
}
}
return html;
} catch (error) {
@@ -130,33 +135,22 @@ export class NavigationManager {
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 { subdirs, itemCount } = this.extractPageData(doc);
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) {
if (itemCount > 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;
}
path = baseUrl + subdirs[0] + '/index.html';
} catch (error) {
break;
}
}
return {
segments: pathSegments,
finalPath: path
};
return { segments: pathSegments, finalPath: path };
}
/**
@@ -177,14 +171,10 @@ export class NavigationManager {
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
}
const data = this.extractPageData(doc);
totalItems = data.itemCount + data.subdirs.length;
subdirs = data.subdirs;
} catch (error) {}
let html = '';
if (finalPath === currentPath) {
@@ -193,15 +183,11 @@ export class NavigationManager {
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;
for (const subdir of subdirs) {
const subPath = baseUrl + subdir + '/index.html';
html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth);
}
}
return html;
}
+14 -2
View File
@@ -260,8 +260,11 @@ def generate(
print(f"Warning: Could not render gallery root: {e}")
# Don't fail, gallery is still usable
if verbose:
print(f"✓ Gallery generated successfully at {gallery_root}")
plot_count = _count_gallery_plots(gallery_root)
if plot_count == 0:
print(f"Warning: Gallery at {gallery_root} appears to be empty — no plot files found!")
else:
print(f"✓ Gallery generated at {gallery_root}{plot_count} plots total")
return True
@@ -271,6 +274,15 @@ def generate(
return False
def _count_gallery_plots(gallery_root: Path) -> int:
"""Recursively count plot files (PDFs and HTMLs, excluding index.html) in the gallery output."""
count = 0
for f in gallery_root.rglob('*'):
if f.is_file() and f.suffix.lower() in ('.pdf', '.html') and f.name != 'index.html':
count += 1
return count
def _is_writable(path: Path) -> bool:
"""
Check if a path is writable.
+1
View File
@@ -0,0 +1 @@
../assets
-71
View File
@@ -1,71 +0,0 @@
/* ========================================
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: 400px; /* Further increased bottom padding to prevent overlap with stats box */
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;
}
/* Responsive design adjustments */
@media (max-width: 768px) {
body {
padding: 0.5rem;
padding-bottom: 300px; /* Further increased mobile bottom padding to match desktop */
}
h1 {
font-size: 1.5rem;
}
h2 {
font-size: 1.2rem;
margin: 1rem 0 0.5rem 0;
}
}
-180
View File
@@ -1,180 +0,0 @@
/* ========================================
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;
}
-440
View File
@@ -1,440 +0,0 @@
/* ========================================
EXPORT FUNCTIONALITY STYLES
======================================== */
/* Selection mode styles */
.selection-mode .grid-item {
cursor: pointer;
transition: all 0.2s ease;
}
.selection-mode .grid-item:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* Selection overlay */
.selection-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
display: none;
justify-content: center;
align-items: center;
border-radius: 8px;
z-index: 5;
}
.selection-mode .selection-overlay {
display: flex;
}
.selection-checkbox {
background: var(--card-background);
border: 2px solid var(--border-color);
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
transition: all 0.2s ease;
}
.grid-item.selected .selection-checkbox {
background: var(--primary-color, #007bff);
border-color: var(--primary-color, #007bff);
color: white;
}
.checkbox-icon {
line-height: 1;
}
/* Selection counter */
.selection-counter {
position: fixed;
bottom: 100px;
right: 20px;
background: var(--card-background);
border: 1px solid var(--border-color);
border-radius: 20px;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
color: var(--text-color);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 999;
}
/* Export button styles */
.export-btn {
background: #28a745 !important;
}
.export-btn:hover {
background: #218838 !important;
}
.export-btn:disabled {
background: #6c757d !important;
cursor: not-allowed;
}
/* Export messages */
.export-message {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 24px;
border-radius: 6px;
font-weight: 500;
z-index: 1001;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: slideIn 0.3s ease;
}
.export-message-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.export-message-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.export-message-warning {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.export-message-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Selection mode indicator */
.selection-mode::before {
content: "Selection Mode - Click plots to select them";
position: fixed;
top: 0;
left: 0;
right: 0;
background: var(--primary-color, #007bff);
color: white;
text-align: center;
padding: 8px;
font-size: 14px;
font-weight: 500;
z-index: 1000;
}
/* Adjust main content when in selection mode */
.selection-mode {
padding-top: 40px;
}
/* Export instructions overlay */
.export-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1002;
}
.export-instructions {
background: var(--card-background);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 24px;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.export-instructions h3 {
margin: 0 0 16px 0;
color: var(--text-color);
}
.export-instructions p {
margin: 0 0 16px 0;
color: var(--text-color);
}
.export-data {
margin: 16px 0;
}
.export-data textarea {
width: 100%;
height: 200px;
font-family: 'Courier New', monospace;
font-size: 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 8px;
background: var(--background-color);
color: var(--text-color);
resize: vertical;
}
.export-commands {
margin: 16px 0;
padding: 12px;
background: var(--header-background);
border-radius: 4px;
border: 1px solid var(--border-color);
}
.export-command-container {
margin: 20px 0;
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
}
.export-command {
background: var(--header-background);
padding: 16px;
border-bottom: 1px solid var(--border-color);
}
.export-command code {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.4;
color: var(--text-color);
word-break: break-all;
display: block;
background: none;
border: none;
padding: 0;
margin: 0;
}
.export-actions {
display: flex;
gap: 8px;
padding: 12px 16px;
background: var(--card-background);
}
.export-actions button {
padding: 8px 16px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--card-background);
color: var(--text-color);
cursor: pointer;
transition: all 0.2s ease;
}
.export-actions button:hover {
background: var(--header-background);
}
.export-actions button:last-child {
background: var(--primary-color, #007bff);
color: white;
border-color: var(--primary-color, #007bff);
}
.export-actions button:last-child:hover {
background: var(--primary-color-dark, #0056b3);
}
.copy-btn, .close-btn {
padding: 8px 16px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--card-background);
color: var(--text-color);
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
display: flex;
align-items: center;
gap: 4px;
}
.copy-btn:hover {
background: var(--primary-color, #007bff);
color: white;
border-color: var(--primary-color, #007bff);
}
.close-btn {
background: #dc3545;
color: white;
border-color: #dc3545;
margin-left: auto;
}
.close-btn:hover {
background: #c82333;
border-color: #bd2130;
}
.export-details, .export-tips {
margin: 20px 0;
padding: 16px;
border-radius: 6px;
border: 1px solid var(--border-color);
}
.export-details {
background: var(--header-background);
}
.export-tips {
background: var(--card-background);
border-color: var(--primary-color, #007bff);
border-left: 4px solid var(--primary-color, #007bff);
}
.export-details h4, .export-tips h4 {
margin: 0 0 12px 0;
color: var(--text-color);
font-size: 16px;
}
.export-details ul, .export-tips ul {
margin: 0;
padding-left: 20px;
color: var(--text-color);
}
.export-details li, .export-tips li {
margin: 8px 0;
line-height: 1.5;
}
.export-details code, .export-tips code {
background: var(--background-color);
padding: 2px 6px;
border-radius: 3px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
border: 1px solid var(--border-color);
}
.export-tips kbd {
background: var(--header-background);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 2px 6px;
font-family: inherit;
font-size: 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.copy-feedback {
position: absolute;
top: 10px;
right: 10px;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
z-index: 1003;
animation: fadeInOut 2s ease-in-out;
}
.copy-feedback-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.copy-feedback-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
@keyframes fadeInOut {
0% { opacity: 0; transform: translateY(-10px); }
20% { opacity: 1; transform: translateY(0); }
80% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-10px); }
}
/* Dark theme adjustments */
[data-theme="dark"] .selection-checkbox {
background: var(--card-background);
border-color: var(--border-color);
}
[data-theme="dark"] .grid-item.selected .selection-checkbox {
background: var(--primary-color, #0d6efd);
border-color: var(--primary-color, #0d6efd);
}
[data-theme="dark"] .export-message-info {
background: #0c5460;
color: #d1ecf1;
border-color: #086972;
}
[data-theme="dark"] .export-message-success {
background: #155724;
color: #d4edda;
border-color: #1e7e34;
}
[data-theme="dark"] .export-message-warning {
background: #856404;
color: #fff3cd;
border-color: #b58b14;
}
[data-theme="dark"] .export-message-error {
background: #721c24;
color: #f8d7da;
border-color: #a94442;
}
[data-theme="dark"] .copy-feedback-success {
background: #155724;
color: #d4edda;
border-color: #1e7e34;
}
[data-theme="dark"] .copy-feedback-error {
background: #721c24;
color: #f8d7da;
border-color: #a94442;
}
[data-theme="dark"] .export-tips kbd {
background: var(--background-color);
color: var(--text-color);
}
-112
View File
@@ -1,112 +0,0 @@
/* ========================================
FLOATING ACTION BUTTONS
======================================== */
.floating-buttons {
position: fixed;
bottom: 80px;
right: 15px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 1000;
/* Ensure buttons don't interfere with content */
pointer-events: none;
}
.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;
/* Re-enable pointer events for buttons */
pointer-events: auto;
}
.floating-btn:hover {
transform: scale(1.1);
}
.sidebar-toggle {
background: var(--button-bg);
color: white;
}
.theme-toggle {
background: var(--button-bg);
color: white;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.floating-buttons {
bottom: 60px;
right: 10px;
gap: 8px;
}
.floating-btn {
width: 48px;
height: 48px;
font-size: 1.1rem;
}
}
/* ========================================
KEYBOARD SHORTCUTS HELP
======================================== */
.shortcuts-help {
position: fixed;
bottom: 220px;
right: 15px;
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: 1001;
font-size: 0.9rem;
max-width: 280px;
max-height: 400px;
overflow-y: auto;
}
.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;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.shortcuts-help {
bottom: 180px;
right: 10px;
left: 10px;
max-width: none;
max-height: 300px;
}
}
-179
View File
@@ -1,179 +0,0 @@
/* ========================================
FOLDER METADATA STYLES
======================================== */
/* Folder Metadata Container */
.folder-metadata-container {
margin: 1rem 0;
border-radius: 8px;
background: var(--card-bg);
border: 1px solid var(--border-color);
overflow: hidden;
}
/* Folder Metadata Toggle Button */
.folder-metadata-toggle {
width: 100%;
padding: 0.75rem 1rem;
background: var(--card-bg);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
transition: background-color 0.2s ease;
font-size: 0.95rem;
color: var(--text-color);
position: relative;
z-index: 10;
outline: none;
}
.folder-metadata-toggle:hover {
background: var(--button-hover);
color: white;
}
.folder-metadata-toggle:focus {
outline: 2px solid var(--link-color);
outline-offset: 2px;
}
.folder-metadata-icon {
margin-right: 0.5rem;
}
.folder-metadata-label {
flex: 1;
text-align: left;
font-weight: 500;
}
.folder-metadata-arrow {
transition: transform 0.2s ease;
font-size: 0.8rem;
}
.folder-metadata-container.expanded .folder-metadata-arrow {
transform: rotate(180deg);
}
/* Folder Metadata Content - HIDDEN BY DEFAULT */
.folder-metadata-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, opacity 0.3s ease;
background: var(--bg-color);
opacity: 0;
display: none; /* Force hide initially */
}
/* Show content when expanded */
.folder-metadata-container.expanded .folder-metadata-content {
max-height: 1000px;
border-top: 1px solid var(--border-color);
opacity: 1;
display: block; /* Show when expanded */
}
/* Folder Metadata Grid */
.folder-metadata-grid {
padding: 1rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.5rem 1rem;
}
/* Folder Metadata Items */
.folder-metadata-item {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.25rem 0;
}
.folder-metadata-key {
font-weight: 600;
color: var(--link-color);
white-space: nowrap;
min-width: fit-content;
}
.folder-metadata-value {
color: var(--text-color);
word-break: break-word;
flex: 1;
}
/* Tags for list items */
.folder-metadata-list {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.folder-metadata-tag {
background: var(--link-color);
color: white;
padding: 0.2rem 0.5rem;
border-radius: 12px;
font-size: 0.8rem;
white-space: nowrap;
}
/* Nested metadata */
.folder-metadata-nested {
background: var(--card-bg);
padding: 0.5rem;
border-radius: 4px;
border-left: 3px solid var(--link-color);
}
.folder-metadata-nested-item {
margin: 0.25rem 0;
font-size: 0.9rem;
}
/* Long text handling */
.folder-metadata-expand {
background: none;
border: none;
color: var(--link-color);
cursor: pointer;
text-decoration: underline;
padding: 0;
margin-left: 0.5rem;
font-size: 0.85rem;
}
.folder-metadata-expand:hover {
color: var(--link-hover);
}
/* Links */
.folder-metadata-value a {
color: var(--link-color);
text-decoration: none;
}
.folder-metadata-value a:hover {
color: var(--link-hover);
text-decoration: underline;
}
/* Responsive design */
@media (max-width: 768px) {
.folder-metadata-grid {
grid-template-columns: 1fr;
gap: 0.5rem;
}
.folder-metadata-item {
flex-direction: column;
gap: 0.25rem;
}
.folder-metadata-key {
white-space: normal;
}
}
-40
View File
@@ -1,40 +0,0 @@
/* ========================================
FOLDER TREE
======================================== */
.folder-tree {
background: var(--tree-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
font-size: 0.85rem;
max-height: 350px;
overflow-y: auto;
transition: all 0.3s ease;
}
.tree-item {
margin: 0.2rem 0;
white-space: pre;
font-family: inherit;
}
.tree-current {
background: var(--tree-current-bg);
color: white;
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-weight: 500;
}
.tree-link {
color: var(--link-color);
text-decoration: none;
transition: color 0.2s ease;
}
.tree-link:hover {
text-decoration: underline;
color: var(--link-hover);
}
-106
View File
@@ -1,106 +0,0 @@
/* ========================================
PLOT GRID
======================================== */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.8rem;
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;
}
-38
View File
@@ -1,38 +0,0 @@
/* Styles for HTML plots */
.plot-item.html-plot .plot-link {
position: relative;
}
.plot-item.html-plot .html-thumbnail {
background: #f5f5f5;
border: 1px solid #ddd;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 200px;
text-align: center;
padding: 20px;
}
.plot-item.html-plot .html-indicator {
position: absolute;
top: 5px;
right: 5px;
background: #4CAF50;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.8em;
}
.plot-item.html-plot .html-preview {
color: #666;
margin-top: 10px;
font-size: 0.9em;
}
/* Add a hover effect */
.plot-item.html-plot .plot-link:hover .html-thumbnail {
background: #e8e8e8;
}
-33
View File
@@ -1,33 +0,0 @@
/* ========================================
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');
@import url('./metadata.css');
@import url('./metadata-section.css');
@import url('./folder-metadata.css');
@import url('./export.css');
/* View controls - must come after grid.css to override */
@import url('./view-controls.css');
/* Sort controls styling */
@import url('./sort-controls.css');
/* View override - force grid layout to work */
@import url('./view-override.css');
/* Responsive design - last to override everything */
@import url('./responsive.css');
-383
View File
@@ -1,383 +0,0 @@
/* ========================================
METADATA SECTION STYLES
======================================== */
/* Metadata Section Container */
.metadata-section {
margin: 1rem 0;
border-radius: 8px;
background: var(--card-bg);
border: 1px solid var(--border-color);
overflow: hidden;
}
/* Toggle Button */
.metadata-toggle-btn {
width: 100%;
padding: 0.75rem 1rem;
background: var(--card-bg);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
transition: background-color 0.2s ease;
font-size: 0.95rem;
color: var(--text-color);
position: relative;
z-index: 10;
}
.metadata-toggle-btn:hover {
background: var(--button-hover);
color: white;
}
.metadata-toggle-btn:focus {
outline: 2px solid var(--link-color);
outline-offset: 2px;
}
.metadata-icon {
margin-right: 0.5rem;
}
.metadata-label {
flex: 1;
text-align: left;
font-weight: 500;
}
.metadata-arrow {
transition: transform 0.2s ease;
font-size: 0.8rem;
}
/* Content Area - Hidden by default */
.metadata-content {
background: var(--bg-color);
border-top: 1px solid var(--border-color);
display: none; /* Hidden by default */
}
/* Metadata Header with File Path */
.metadata-header {
padding: 1rem;
background: var(--card-bg);
border-bottom: 1px solid var(--border-color);
}
.metadata-file-info {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.file-path-label {
font-weight: 600;
color: var(--text-color);
white-space: nowrap;
font-size: 0.9rem;
}
.file-path {
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 0.4rem 0.6rem;
font-family: 'Courier New', monospace;
font-size: 0.8rem;
color: var(--link-color);
flex: 1;
min-width: 200px;
word-break: break-all;
user-select: all;
}
.copy-path-btn {
background: var(--link-color);
color: white;
border: none;
border-radius: 4px;
padding: 0.4rem 0.8rem;
cursor: pointer;
font-size: 0.8rem;
transition: all 0.2s ease;
white-space: nowrap;
font-weight: 500;
}
.copy-path-btn:hover {
background: var(--link-hover);
transform: translateY(-1px);
}
.copy-path-btn:active {
transform: scale(0.95);
}
.copy-path-btn.copied {
background: #4CAF50;
transform: scale(1.05);
}
/* Tip Icon with Hover Tooltip */
.tip-icon {
cursor: help;
font-size: 1.2rem;
position: relative;
display: inline-block;
margin-left: 0.25rem;
opacity: 0.8;
transition: opacity 0.2s ease;
}
.tip-icon:hover {
opacity: 1;
}
/* Custom tooltip for tip icon */
.tip-icon::after {
content: attr(title);
position: absolute;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
white-space: normal;
width: 280px;
text-align: left;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
line-height: 1.4;
font-weight: normal;
font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
}
/* Tooltip arrow */
.tip-icon::before {
content: '';
position: absolute;
bottom: 115%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #333;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
z-index: 1001;
}
.tip-icon:hover::after,
.tip-icon:hover::before {
opacity: 1;
visibility: visible;
}
/* Grid Layout */
.metadata-grid {
padding: 1rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem 1.5rem; /* Increased gaps to prevent overlapping */
}
/* Metadata Items */
.metadata-item {
display: flex;
flex-direction: column; /* Stack key and value vertically to prevent overlap */
gap: 0.25rem;
padding: 0.75rem;
background: var(--card-bg);
border-radius: 6px;
border: 1px solid var(--border-color);
word-wrap: break-word; /* Ensure long text wraps */
overflow-wrap: break-word; /* Additional word wrapping */
}
.metadata-key {
font-weight: 600;
color: var(--link-color);
font-size: 0.9rem;
margin-bottom: 0.25rem;
}
.metadata-value {
color: var(--text-color);
word-break: break-word;
overflow-wrap: break-word;
line-height: 1.4;
font-size: 0.9rem;
}
/* LaTeX content styling */
.latex-content {
color: var(--text-color);
line-height: 1.6;
font-family: 'Times New Roman', serif;
}
/* Simple list items - proper list formatting */
.metadata-list {
color: var(--text-color);
line-height: 1.4;
margin: 0;
padding-left: 1.2rem;
list-style-type: disc; /* Add bullet points */
}
.metadata-list li {
margin: 0.25rem 0;
padding: 0;
color: var(--text-color);
}
/* YAML-style formatting for metadata */
.metadata-yaml-list {
margin: 0.5rem 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.9rem;
line-height: 1.6;
background: var(--card-bg);
padding: 0.8rem;
border-radius: 6px;
border: 1px solid var(--border-color);
}
.yaml-list-item {
color: var(--text-color);
margin: 0.25rem 0;
padding-left: 0;
text-indent: 0;
}
.metadata-yaml-object {
margin: 0.5rem 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.9rem;
line-height: 1.6;
background: var(--card-bg);
padding: 0.8rem;
border-radius: 6px;
border: 1px solid var(--border-color);
}
.yaml-object-item {
margin: 0.5rem 0;
}
.yaml-key {
color: var(--link-color);
font-weight: 600;
}
.yaml-value {
color: var(--text-color);
margin-left: 0.5rem;
}
.yaml-nested-list {
margin: 0.25rem 0 0 1.5rem;
border-left: 2px solid var(--border-color);
padding-left: 0.8rem;
}
.yaml-nested-item {
color: var(--text-color);
margin: 0.2rem 0;
padding-left: 0;
}
.yaml-nested-object {
margin: 0.25rem 0 0 1.5rem;
border-left: 2px solid var(--border-color);
padding-left: 0.8rem;
}
/* Remove the blue box styling for tags */
.metadata-tag {
display: inline;
background: none;
color: var(--text-color);
padding: 0;
border-radius: 0;
font-size: inherit;
white-space: normal;
}
/* Simplified nested metadata */
.metadata-nested {
background: none;
padding: 0;
border-radius: 0;
border-left: none;
color: var(--text-color);
}
.metadata-nested-item {
margin: 0.25rem 0;
font-size: 0.9rem;
}
/* Long text handling */
.metadata-expand {
background: none;
border: none;
color: var(--link-color);
cursor: pointer;
text-decoration: underline;
padding: 0;
margin-left: 0.5rem;
font-size: 0.85rem;
}
.metadata-expand:hover {
color: var(--link-hover);
}
/* Links */
.metadata-value a {
color: var(--link-color);
text-decoration: none;
}
.metadata-value a:hover {
color: var(--link-hover);
text-decoration: underline;
}
/* Responsive design */
@media (max-width: 768px) {
.metadata-grid {
grid-template-columns: 1fr;
gap: 0.5rem;
}
.metadata-item {
flex-direction: column;
gap: 0.25rem;
}
.metadata-key {
white-space: normal;
}
.metadata-file-info {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
.file-path {
min-width: auto;
}
}
-180
View File
@@ -1,180 +0,0 @@
/* ========================================
METADATA POPUP STYLES
======================================== */
/* Metadata button on thumbnails */
.metadata-btn {
position: absolute;
top: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.7);
color: white;
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
font-size: 16px;
cursor: pointer;
z-index: 10;
transition: all 0.2s ease;
backdrop-filter: blur(4px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
opacity: 0;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.grid-item:hover .metadata-btn {
opacity: 1;
}
.metadata-btn:hover {
background: rgba(0, 0, 0, 0.9);
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.metadata-btn:active {
transform: scale(0.95);
}
/* Grid item positioning for metadata button */
.grid-item {
position: relative;
}
/* Metadata popup */
.metadata-popup {
background: var(--card-background);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
max-width: 320px;
min-width: 250px;
opacity: 0;
transform: translateY(-10px);
transition: all 0.2s ease;
backdrop-filter: blur(10px);
z-index: 1000;
}
.metadata-popup.show {
opacity: 1;
transform: translateY(0);
}
.metadata-popup-header {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
background: var(--header-background);
border-radius: 8px 8px 0 0;
}
.metadata-popup-header h4 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: var(--text-color);
word-break: break-word;
}
.metadata-popup-content {
padding: 12px 16px;
max-height: 300px;
overflow-y: auto;
}
.metadata-field {
display: flex;
margin-bottom: 8px;
gap: 8px;
align-items: flex-start;
}
.metadata-field:last-child {
margin-bottom: 0;
}
.metadata-key {
font-weight: 500;
color: var(--accent-color);
font-size: 12px;
min-width: 80px;
flex-shrink: 0;
}
.metadata-value {
font-size: 12px;
color: var(--text-color);
word-break: break-word;
flex: 1;
}
.metadata-value code {
background: var(--code-background);
padding: 2px 4px;
border-radius: 3px;
font-size: 11px;
font-family: 'Courier New', monospace;
}
.metadata-tag {
background: var(--accent-color);
color: var(--background-color);
padding: 2px 6px;
border-radius: 12px;
font-size: 10px;
font-weight: 500;
margin-right: 4px;
display: inline-block;
}
.metadata-more {
color: var(--breadcrumb-color);
font-style: italic;
font-size: 11px;
}
.no-metadata {
color: var(--breadcrumb-color);
font-style: italic;
margin: 0;
text-align: center;
padding: 20px 0;
}
/* Custom scrollbar for metadata popup */
.metadata-popup-content::-webkit-scrollbar {
width: 6px;
}
.metadata-popup-content::-webkit-scrollbar-track {
background: transparent;
}
.metadata-popup-content::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
.metadata-popup-content::-webkit-scrollbar-thumb:hover {
background: var(--accent-color);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.metadata-popup {
max-width: calc(100vw - 32px);
min-width: 200px;
}
.metadata-btn {
width: 28px;
height: 28px;
font-size: 14px;
top: 6px;
right: 6px;
}
}
-57
View File
@@ -1,57 +0,0 @@
/* ========================================
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;
}
-59
View File
@@ -1,59 +0,0 @@
/* ========================================
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%;
}
/* View controls responsive */
.controls-container {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.sort-controls {
justify-content: center;
}
.view-controls {
margin-right: 0.5rem !important;
padding: 8px 12px !important;
justify-content: center;
}
.view-btn {
min-width: 40px !important;
min-height: 40px !important;
padding: 8px 12px !important;
}
.view-btn svg {
width: 16px !important;
height: 16px !important;
}
}
-72
View File
@@ -1,72 +0,0 @@
/* ========================================
SEARCH FUNCTIONALITY
======================================== */
.search-container {
position: relative;
max-width: 500px;
margin: 1rem 0;
}
.search-box {
width: 100%;
padding: 0.8rem 3rem 0.8rem 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--card-bg);
color: var(--text-color);
font-size: 1rem;
outline: none;
transition: all 0.3s ease;
}
.search-box:focus {
border-color: var(--button-bg);
box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.1);
}
.search-icon {
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-color);
opacity: 0.6;
pointer-events: none;
}
.search-results {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
margin-top: 0.5rem;
max-height: 400px;
overflow-y: auto;
display: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 100;
}
.search-result-item {
padding: 0.75rem;
margin: 0;
cursor: pointer;
transition: background-color 0.2s ease;
border-bottom: 1px solid var(--border-color);
}
.search-result-item:last-child {
border-bottom: none;
}
.search-result-item:hover {
background: var(--button-bg);
color: white;
}
.search-highlight {
background: #ffeb3b;
color: #000;
font-weight: bold;
padding: 0.1rem 0.2rem;
border-radius: 2px;
}
-121
View File
@@ -1,121 +0,0 @@
/* ========================================
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;
}
-88
View File
@@ -1,88 +0,0 @@
/* ========================================
SORT CONTROLS STYLING
======================================== */
/* Clean styling for sort controls */
.sort-controls {
display: flex !important;
align-items: center !important;
gap: 8px !important;
padding: 12px 16px !important;
background: var(--tree-bg) !important;
border: 1px solid var(--border-color) !important;
border-radius: 8px !important;
margin-right: 1rem !important;
}
.sort-label {
font-size: 0.9rem !important;
color: var(--text-color) !important;
margin-right: 8px !important;
font-weight: 500 !important;
}
/* Button styling using theme variables */
.sort-btn {
background: var(--card-bg) !important;
border: 1px solid var(--border-color) !important;
border-radius: 6px !important;
padding: 8px 12px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
font-size: 0.85rem !important;
color: var(--text-color) !important;
display: flex !important;
align-items: center !important;
gap: 4px !important;
outline: none !important;
text-decoration: none !important;
font-family: inherit !important;
}
.sort-btn:hover {
background: var(--button-bg) !important;
color: white !important;
transform: translateY(-1px) !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important;
border-color: var(--button-bg) !important;
}
.sort-btn.active {
background: var(--button-bg) !important;
color: white !important;
border-color: var(--button-bg) !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important;
}
.sort-order-btn {
background: var(--card-bg) !important;
border: 1px solid var(--border-color) !important;
border-radius: 6px !important;
padding: 8px 12px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
font-size: 1rem !important;
color: var(--text-color) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
min-width: 36px !important;
outline: none !important;
text-decoration: none !important;
font-family: inherit !important;
}
.sort-order-btn:hover {
background: var(--button-bg) !important;
color: white !important;
transform: translateY(-1px) !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important;
border-color: var(--button-bg) !important;
}
.sort-order-btn.active {
background: var(--button-bg) !important;
color: white !important;
border-color: var(--button-bg) !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important;
}
View File
-59
View File
@@ -1,59 +0,0 @@
/* ========================================
GALLERY STATISTICS
======================================== */
.gallery-stats {
position: fixed;
bottom: 80px;
left: 15px;
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;
/* Ensure stats don't interfere with content */
pointer-events: none;
}
.gallery-stats:hover {
opacity: 1;
/* Re-enable pointer events on hover */
pointer-events: auto;
}
.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);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.gallery-stats {
bottom: 60px;
left: 10px;
padding: 0.6rem 0.8rem;
font-size: 0.8rem;
max-width: 180px;
}
.stats-item {
margin: 0.15rem 0;
}
}
-33
View File
@@ -1,33 +0,0 @@
/* ========================================
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;
}
-404
View File
@@ -1,404 +0,0 @@
/* ========================================
VIEW CONTROLS AND LAYOUT MODES
======================================== */
/* Controls Container */
.controls-container {
display: flex;
justify-content: space-between;
align-items: center;
margin: 1rem 0;
gap: 2rem;
flex-wrap: wrap;
}
/* Sort Controls */
.sort-controls {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--tree-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
}
.sort-label {
font-size: 0.9rem;
color: var(--text-color);
margin-right: 4px;
font-weight: 500;
}
.sort-btn, .sort-order-btn {
background: var(--card-bg) !important;
border: 1px solid var(--border-color) !important;
border-radius: 6px !important;
padding: 6px 12px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
font-size: 0.85rem !important;
color: var(--text-color) !important;
display: flex !important;
align-items: center !important;
gap: 4px !important;
outline: none !important;
text-decoration: none !important;
}
.sort-btn:hover, .sort-order-btn:hover {
background: var(--button-bg) !important;
color: white !important;
transform: translateY(-1px) !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
}
.sort-btn.active, .sort-order-btn.active {
background: var(--button-bg) !important;
color: white !important;
border-color: var(--button-bg) !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
}
.sort-order-btn {
min-width: 32px !important;
justify-content: center !important;
font-size: 1rem !important;
}
/* View Controls */
.view-controls {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--tree-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
position: relative;
z-index: 10;
}
.view-btn {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 12px 16px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 1.2rem;
color: var(--text-color);
min-width: 48px;
min-height: 48px;
display: flex;
align-items: center;
justify-content: center;
}
.view-btn svg {
width: 18px;
height: 18px;
}
.view-btn:hover {
background: var(--button-bg);
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.view-btn.active {
background: var(--button-bg);
color: white;
border-color: var(--button-bg);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Plot Container Base Styles */
.plot-container {
margin: 1rem 0;
margin-bottom: 450px; /* Add extra bottom margin to prevent overlap with stats box */
transition: all 0.3s ease;
clear: both;
}
.plot-container .plot-item {
transition: all 0.2s ease;
border-radius: 8px;
overflow: hidden;
background: var(--card-bg);
border: 1px solid transparent;
}
.plot-container .plot-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border-color: var(--border-color);
}
.plot-container .plot-link {
color: var(--link-color);
text-decoration: none;
display: block;
}
.plot-container .plot-thumbnail {
width: 100%;
height: auto;
border-radius: 6px;
transition: all 0.2s ease;
}
.plot-container .plot-info {
padding: 0.8rem;
}
.plot-container .plot-name {
word-wrap: break-word;
word-break: break-word;
hyphens: auto;
font-size: 0.9rem;
line-height: 1.3;
font-weight: 500;
color: var(--text-color);
}
.plot-container .plot-date {
font-size: 0.8rem;
color: var(--text-secondary);
margin-top: 0.3rem;
opacity: 0.7; /* Slightly lower opacity for differentiation */
}
/* Grid View - Override any conflicting styles */
.plot-container.grid-view {
display: grid !important;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)) !important;
gap: 1.2rem !important;
padding: 1rem 0 !important;
}
.plot-container.grid-view .plot-item,
.plot-container.grid-view .grid-item {
text-align: center !important;
background: var(--card-bg) !important;
border-radius: 8px !important;
padding: 0.8rem !important;
transition: all 0.2s ease !important;
border: 1px solid transparent !important;
display: block !important;
width: auto !important;
max-width: none !important;
}
.plot-container.grid-view .plot-item:hover,
.plot-container.grid-view .grid-item:hover {
transform: translateY(-2px) !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.1) !important;
border-color: var(--border-color) !important;
}
.plot-container.grid-view .plot-link {
display: block !important;
color: var(--link-color) !important;
text-decoration: none !important;
}
.plot-container.grid-view .plot-thumbnail {
max-width: 100% !important;
height: auto !important;
border: 1px solid var(--border-color) !important;
display: block !important;
width: 100% !important;
object-fit: contain !important;
border-radius: 6px !important;
}
.plot-container.grid-view .plot-info {
padding: 0.8rem 0 0 0 !important;
}
.plot-container.grid-view .plot-name {
max-height: 3.9rem !important;
overflow: hidden !important;
margin-top: 0.8rem !important;
display: block !important;
word-wrap: break-word !important;
word-break: break-word !important;
hyphens: auto !important;
font-size: 0.9rem !important;
line-height: 1.3 !important;
font-weight: 500 !important;
color: var(--text-color) !important;
text-align: center !important;
}
/* Large List View */
.plot-container.list-large-view {
display: flex !important;
flex-direction: column !important;
gap: 0.8rem !important;
}
.plot-container.list-large-view .plot-item {
display: flex !important;
align-items: center !important;
padding: 1rem !important;
gap: 1rem !important;
}
.plot-container.list-large-view .plot-link {
flex-shrink: 0 !important;
width: 120px !important;
height: 90px !important;
overflow: hidden !important;
border-radius: 6px !important;
border: 1px solid var(--border-color) !important;
}
.plot-container.list-large-view .plot-thumbnail {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
}
.plot-container.list-large-view .plot-info {
flex: 1 !important;
padding: 0 !important;
text-align: left !important;
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
}
.plot-container.list-large-view .plot-name {
font-size: 1rem !important;
line-height: 1.4 !important;
max-height: none !important;
overflow: visible !important;
flex: 1 !important;
}
.plot-container.list-large-view .plot-date {
flex-shrink: 0 !important;
margin-left: 1rem !important;
margin-top: 0 !important;
font-size: 0.85rem !important;
color: var(--text-secondary) !important;
white-space: nowrap !important;
}
/* Compact List View */
.plot-container.list-compact-view {
display: flex !important;
flex-direction: column !important;
gap: 0.4rem !important;
}
.plot-container.list-compact-view .plot-item {
display: flex !important;
align-items: center !important;
padding: 0.6rem 1rem !important;
gap: 0.8rem !important;
border-radius: 4px !important;
}
.plot-container.list-compact-view .plot-link {
flex: 1 !important;
display: flex !important;
align-items: center !important;
}
.plot-container.list-compact-view .plot-thumbnail {
display: none !important;
}
.plot-container.list-compact-view .plot-info {
padding: 0 !important;
flex: 1 !important;
text-align: left !important;
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
}
.plot-container.list-compact-view .plot-name {
font-size: 0.95rem !important;
line-height: 1.2 !important;
margin: 0 !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
flex: 1 !important;
}
.plot-container.list-compact-view .plot-date {
flex-shrink: 0 !important;
margin-left: 1rem !important;
margin-top: 0 !important;
font-size: 0.8rem !important;
color: var(--text-secondary) !important;
white-space: nowrap !important;
}
/* Highlight effect for all views */
.plot-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); }
}
/* Responsive adjustments */
@media (max-width: 768px) {
.view-controls {
width: 100%;
margin-left: 0;
margin-right: 0;
}
.plot-container.grid-view {
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1rem;
}
.plot-container.list-large-view .plot-link {
width: 80px;
height: 60px;
}
.plot-container.list-large-view .plot-item {
padding: 0.8rem;
}
.plot-container.list-compact-view .plot-item {
padding: 0.5rem 0.8rem;
}
}
@media (max-width: 480px) {
.view-controls {
gap: 4px;
}
.view-btn {
padding: 6px 8px;
font-size: 1rem;
min-width: 32px;
}
.plot-container.grid-view {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.plot-container.list-large-view .plot-link {
width: 60px;
height: 45px;
}
}
-54
View File
@@ -1,54 +0,0 @@
/* ========================================
VIEW OVERRIDE - Ensure grid view works
======================================== */
/* Force grid layout when grid-view class is present */
body .plot-container.grid-view {
display: grid !important;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)) !important;
gap: 1.2rem !important;
padding: 1rem 0 !important;
}
/* Force grid items to be proper tiles */
body .plot-container.grid-view .plot-item,
body .plot-container.grid-view .grid-item {
display: block !important;
width: auto !important;
max-width: none !important;
text-align: center !important;
background: var(--card-bg) !important;
border-radius: 8px !important;
padding: 0.8rem !important;
border: 1px solid transparent !important;
}
/* Ensure thumbnails are properly sized */
body .plot-container.grid-view .plot-thumbnail {
width: 100% !important;
height: auto !important;
max-width: 100% !important;
display: block !important;
border: 1px solid var(--border-color) !important;
border-radius: 6px !important;
object-fit: cover !important;
aspect-ratio: 4/3;
}
/* Force plot info styling */
body .plot-container.grid-view .plot-info {
padding: 0.5rem 0 0 0 !important;
text-align: center !important;
}
body .plot-container.grid-view .plot-name {
font-size: 0.85rem !important;
line-height: 1.2 !important;
max-height: 2.4rem !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
display: -webkit-box !important;
-webkit-line-clamp: 2 !important;
line-clamp: 2 !important;
-webkit-box-orient: vertical !important;
}
-180
View File
@@ -1,180 +0,0 @@
/**
* 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);
}
}
-448
View File
@@ -1,448 +0,0 @@
/**
* Export Manager for Gallery
* Handles exporting selected plots to merged PDF
*/
export class ExportManager {
constructor() {
this.selectedPlots = new Set();
this.maxPlots = 4;
this.init();
}
init() {
this.createExportButton();
this.bindEvents();
}
/**
* Create the export button in the floating buttons section
*/
createExportButton() {
const floatingButtons = document.querySelector('.floating-buttons');
if (!floatingButtons) return;
const exportBtn = document.createElement('button');
exportBtn.className = 'floating-btn export-btn';
exportBtn.id = 'exportBtn';
exportBtn.title = 'Export Selected Plots (Ctrl+E)';
exportBtn.innerHTML = '📄';
exportBtn.style.display = 'none'; // Hidden by default
exportBtn.onclick = () => this.exportSelectedPlots();
floatingButtons.appendChild(exportBtn);
// Add selection counter
const selectionCounter = document.createElement('div');
selectionCounter.className = 'selection-counter';
selectionCounter.id = 'selectionCounter';
selectionCounter.style.display = 'none';
selectionCounter.innerHTML = '0/4 selected';
floatingButtons.appendChild(selectionCounter);
}
/**
* Bind events for plot selection
*/
bindEvents() {
// Add selection mode toggle
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'e') {
e.preventDefault();
this.toggleSelectionMode();
}
if (e.key === 'Escape') {
this.exitSelectionMode();
}
});
// Add selection handlers to existing plots
this.addSelectionHandlers();
}
/**
* Add selection handlers to all plot items
*/
addSelectionHandlers() {
const plotItems = document.querySelectorAll('.grid-item');
plotItems.forEach(item => this.addSelectionHandler(item));
}
/**
* Add selection handler to a single plot item
*/
addSelectionHandler(item) {
// Create selection overlay
const overlay = document.createElement('div');
overlay.className = 'selection-overlay';
overlay.innerHTML = `
<div class="selection-checkbox">
<span class="checkbox-icon">☐</span>
</div>
`;
item.appendChild(overlay);
// Add click handler for selection
overlay.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.togglePlotSelection(item);
});
}
/**
* Toggle selection mode
*/
toggleSelectionMode() {
const body = document.body;
const isSelectionMode = body.classList.contains('selection-mode');
if (isSelectionMode) {
this.exitSelectionMode();
} else {
this.enterSelectionMode();
}
}
/**
* Enter selection mode
*/
enterSelectionMode() {
document.body.classList.add('selection-mode');
document.getElementById('exportBtn').style.display = 'block';
document.getElementById('selectionCounter').style.display = 'block';
this.updateSelectionCounter();
}
/**
* Exit selection mode
*/
exitSelectionMode() {
const wasInSelectionMode = document.body.classList.contains('selection-mode');
document.body.classList.remove('selection-mode');
document.getElementById('exportBtn').style.display = 'none';
document.getElementById('selectionCounter').style.display = 'none';
this.clearSelection();
// Show message if user was actually in selection mode
if (wasInSelectionMode) {
this.showMessage('Exited selection mode', 'info');
}
}
/**
* Toggle plot selection
*/
togglePlotSelection(item) {
const plotName = this.getPlotName(item);
const plotPath = this.getPlotPath(item);
if (this.selectedPlots.has(plotName)) {
this.selectedPlots.delete(plotName);
item.classList.remove('selected');
item.querySelector('.checkbox-icon').textContent = '☐';
} else {
if (this.selectedPlots.size >= this.maxPlots) {
this.showMessage(`Maximum ${this.maxPlots} plots can be selected`, 'warning');
return;
}
this.selectedPlots.add(plotName);
item.classList.add('selected');
item.querySelector('.checkbox-icon').textContent = '☑';
}
this.updateSelectionCounter();
}
/**
* Get plot name from grid item
*/
getPlotName(item) {
const plotName = item.querySelector('.plot-name');
return plotName ? plotName.textContent.trim() : '';
}
/**
* Get plot PDF path from grid item
*/
getPlotPath(item) {
const link = item.querySelector('a[href$=".pdf"]');
return link ? link.href : '';
}
/**
* Update selection counter
*/
updateSelectionCounter() {
const counter = document.getElementById('selectionCounter');
if (counter) {
counter.textContent = `${this.selectedPlots.size}/${this.maxPlots} selected`;
}
const exportBtn = document.getElementById('exportBtn');
if (exportBtn) {
exportBtn.disabled = this.selectedPlots.size === 0;
exportBtn.style.opacity = this.selectedPlots.size === 0 ? '0.5' : '1';
}
}
/**
* Clear all selections
*/
clearSelection() {
this.selectedPlots.clear();
document.querySelectorAll('.grid-item.selected').forEach(item => {
item.classList.remove('selected');
const checkbox = item.querySelector('.checkbox-icon');
if (checkbox) checkbox.textContent = '☐';
});
this.updateSelectionCounter();
}
/**
* Export selected plots to merged PDF
*/
async exportSelectedPlots() {
if (this.selectedPlots.size === 0) {
this.showMessage('No plots selected', 'warning');
return;
}
const plotPaths = Array.from(this.selectedPlots).map(plotName => {
const item = Array.from(document.querySelectorAll('.grid-item'))
.find(item => this.getPlotName(item) === plotName);
return this.getPlotPath(item);
});
this.showMessage('Preparing export...', 'info');
try {
await this.createMergedPDF(plotPaths);
} catch (error) {
this.showMessage('Export failed: ' + error.message, 'error');
}
}
/**
* Create merged PDF using Python script
*/
async createMergedPDF(plotPaths) {
// Convert file:// URLs to actual paths
const actualPaths = plotPaths.map(url => {
if (url.startsWith('file://')) {
return url.substring(7); // Remove 'file://' prefix
}
return url;
});
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
const outputName = `merged_plots_${timestamp}.pdf`;
const payload = {
plots: actualPaths,
layout: this.calculateLayout(actualPaths.length),
output_name: outputName
};
// Generate a unique temporary filename
const tempFileName = `export_request_${Date.now()}.json`;
// Get work directory from config or fallback
const workDir = window.galleryConfig?.workDir || '/work/kschmidt/web';
// Save the request to a JSON file that can be picked up by a Python script
const requestData = JSON.stringify(payload, null, 2);
// Show improved export instructions with full command
this.showExportInstructions(requestData, tempFileName, workDir);
}
/**
* Calculate optimal layout for given number of plots
*/
calculateLayout(numPlots) {
switch (numPlots) {
case 1: return { rows: 1, cols: 1 };
case 2: return { rows: 1, cols: 2 };
case 3: return { rows: 2, cols: 2 }; // 3 plots in 2x2 grid with one empty
case 4: return { rows: 2, cols: 2 };
default: return { rows: 2, cols: 2 };
}
}
/**
* Show export instructions to user
*/
showExportInstructions(requestData, tempFileName, workDir) {
const tempFilePath = `/tmp/${tempFileName}`;
const fullCommand = `echo '${requestData.replace(/'/g, "'\\''")}' > ${tempFilePath} && cd ${workDir} && python export_plots.py ${tempFilePath}`;
const instructions = `
<div class="export-instructions">
<h3>🚀 Export Selected Plots</h3>
<p>Run the following command in your terminal to export the selected plots:</p>
<div class="export-command-container">
<div class="export-command">
<code id="exportCommand">${fullCommand}</code>
</div>
<div class="export-actions">
<button onclick="this.copyCommand()" class="copy-btn" title="Copy command to clipboard">
📋 Copy Command
</button>
<button onclick="this.copyJSON()" class="copy-btn" title="Copy JSON only">
📄 Copy JSON
</button>
<button onclick="this.close()" class="close-btn">
✕ Close
</button>
</div>
</div>
<div class="export-details">
<h4>📋 Command Breakdown:</h4>
<ul>
<li><strong>Creates temporary file:</strong> <code>${tempFilePath}</code></li>
<li><strong>Changes to work directory:</strong> <code>${workDir}</code></li>
<li><strong>Runs export script:</strong> <code>python export_plots.py</code></li>
<li><strong>Output file:</strong> Will be saved in the work directory</li>
</ul>
</div>
<div class="export-tips">
<h4>💡 Tips:</h4>
<ul>
<li>The temporary JSON file will be automatically cleaned up after successful export</li>
<li>Use <kbd>Esc</kbd> to exit selection mode</li>
<li>Press <kbd>Ctrl+E</kbd> to toggle selection mode</li>
</ul>
</div>
</div>
`;
const overlay = document.createElement('div');
overlay.className = 'export-overlay';
overlay.innerHTML = instructions;
// Add methods to the overlay for button handlers
overlay.copyCommand = function() {
navigator.clipboard.writeText(fullCommand).then(() => {
this.showCopyFeedback('Command copied to clipboard!');
}).catch(() => {
this.showCopyFeedback('Failed to copy. Please select and copy manually.', 'error');
});
};
overlay.copyJSON = function() {
navigator.clipboard.writeText(requestData).then(() => {
this.showCopyFeedback('JSON copied to clipboard!');
}).catch(() => {
this.showCopyFeedback('Failed to copy. Please select and copy manually.', 'error');
});
};
overlay.close = function() {
this.remove();
};
overlay.showCopyFeedback = function(message, type = 'success') {
const feedback = document.createElement('div');
feedback.className = `copy-feedback copy-feedback-${type}`;
feedback.textContent = message;
this.appendChild(feedback);
setTimeout(() => {
if (feedback.parentNode) {
feedback.parentNode.removeChild(feedback);
}
}, 2000);
};
document.body.appendChild(overlay);
// Close on ESC key
const handleEscape = (e) => {
if (e.key === 'Escape') {
overlay.remove();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
// Close on clicking outside
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.remove();
document.removeEventListener('keydown', handleEscape);
}
});
}
/**
* Download the PDF blob
*/
downloadPDF(blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `merged_plots_${new Date().toISOString().split('T')[0]}.pdf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
* Show temporary message to user
*/
showMessage(text, type = 'info') {
// Remove existing message
const existing = document.querySelector('.export-message');
if (existing) existing.remove();
const message = document.createElement('div');
message.className = `export-message export-message-${type}`;
message.textContent = text;
document.body.appendChild(message);
setTimeout(() => {
if (message.parentNode) {
message.parentNode.removeChild(message);
}
}, 3000);
}
}
// Add these methods to ExportManager if not present
ExportManager.prototype.isSelectionModeActive = function() {
return document.body.classList.contains('selection-mode');
};
ExportManager.prototype.exitSelectionMode = function() {
document.body.classList.remove('selection-mode');
if (typeof this.clearSelection === 'function') {
this.clearSelection();
}
};
// Ensure a single global instance
window.exportManager = window.exportManager || new ExportManager();
// Listen for ESC key globally to exit selection mode
// (This will work even if focus is not on a plot)
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && window.exportManager && window.exportManager.isSelectionModeActive()) {
window.exportManager.exitSelectionMode();
}
});
// Attach improved export logic to export button
document.addEventListener('DOMContentLoaded', function() {
const exportBtn = document.getElementById('exportBtn');
if (exportBtn) {
exportBtn.addEventListener('click', function() {
window.exportManager.exportSelectedPlots();
});
}
});
-81
View File
@@ -1,81 +0,0 @@
/**
* Folder Metadata functionality for Gallery
*
* Handles folder metadata dropdown display and interaction
*/
// Define function immediately (not waiting for DOM)
window.toggleFolderMetadata = function() {
console.log('toggleFolderMetadata called');
const container = document.querySelector('.folder-metadata-container');
const content = document.getElementById('folderMetadataContent');
if (!container) {
console.log('No metadata container found');
return;
}
if (!content) {
console.log('No metadata content found');
return;
}
const isExpanded = container.classList.contains('expanded');
console.log('Current state - expanded:', isExpanded);
if (isExpanded) {
// Collapse
container.classList.remove('expanded');
content.style.display = 'none';
console.log('Collapsed dropdown');
} else {
// Expand
container.classList.add('expanded');
content.style.display = 'block';
console.log('Expanded dropdown');
}
// Save state
localStorage.setItem('folderMetadataExpanded', (!isExpanded).toString());
};
// Also define as regular function for alternative access
function toggleFolderMetadata() {
window.toggleFolderMetadata();
}
// Toggle long text display
window.toggleMetadataText = function(button) {
const longText = button.previousElementSibling;
const fullText = button.nextElementSibling;
if (fullText.style.display === 'none') {
longText.style.display = 'none';
fullText.style.display = 'inline';
button.textContent = 'Show less';
} else {
longText.style.display = 'inline';
fullText.style.display = 'none';
button.textContent = 'Show more';
}
};
// Initialize folder metadata on page load
document.addEventListener('DOMContentLoaded', function() {
console.log('Folder metadata script loaded');
// Make sure all containers start collapsed
const containers = document.querySelectorAll('.folder-metadata-container');
console.log('Found', containers.length, 'metadata containers');
containers.forEach(container => {
const content = container.querySelector('.folder-metadata-content');
if (content) {
// Force initial hidden state
container.classList.remove('expanded');
content.style.display = 'none';
console.log('Initialized container as collapsed');
}
});
});
-73
View File
@@ -1,73 +0,0 @@
/**
* 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 { ViewManager } from './view-manager.js';
import { SortManager } from './sort-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.viewManager = new ViewManager();
this.sortManager = new SortManager();
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.viewManager = this.viewManager;
window.sortManager = this.sortManager;
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(); }
toggleCompareMode() { this.comparisonManager.toggleCompareMode(); }
closeComparison() { this.comparisonManager.closeComparison(); }
selectPlotForComparison(slot) { this.comparisonManager.selectPlotForComparison(slot); }
replacePlot(slot) { this.comparisonManager.replacePlot(slot); }
}
-130
View File
@@ -1,130 +0,0 @@
/**
* 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.ctrlKey && e.key === 'v') {
e.preventDefault();
if (this.app.viewManager) {
this.app.viewManager.cycleView();
}
}
if (e.ctrlKey && e.key === 'n') {
e.preventDefault();
if (this.app.sortManager) {
this.app.sortManager.setSortType('name');
}
}
if (e.ctrlKey && e.key === 'm') {
e.preventDefault();
if (this.app.sortManager) {
this.app.sortManager.setSortType('time');
}
}
if (e.ctrlKey && e.key === 'o') {
e.preventDefault();
if (this.app.sortManager) {
this.app.sortManager.toggleSortOrder();
}
}
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';
}
}
-27
View File
@@ -1,27 +0,0 @@
/**
* 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(); }
// Make functions globally available
window.toggleTheme = toggleTheme;
window.toggleSidebar = toggleSidebar;
// 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;
});
-212
View File
@@ -1,212 +0,0 @@
/**
* Metadata Popup functionality for Gallery
*
* Handles showing metadata in small popups overlaid on plot thumbnails
*/
class MetadataPopup {
constructor() {
this.activePopup = null;
this.init();
}
init() {
// Close popup when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.metadata-btn') && !e.target.closest('.metadata-popup')) {
this.hidePopup();
}
});
// Close popup on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.hidePopup();
}
});
}
showPopup(button, plotName, metadata) {
// Hide any existing popup
this.hidePopup();
// Create popup element
const popup = document.createElement('div');
popup.className = 'metadata-popup';
popup.innerHTML = this.formatMetadata(plotName, metadata);
// Position popup relative to button
const rect = button.getBoundingClientRect();
popup.style.position = 'fixed';
popup.style.left = rect.left + 'px';
popup.style.top = (rect.bottom + 5) + 'px';
popup.style.zIndex = '1000';
// Add to DOM
document.body.appendChild(popup);
this.activePopup = popup;
// Adjust position if popup goes off screen
setTimeout(() => {
const popupRect = popup.getBoundingClientRect();
// Adjust horizontal position
if (popupRect.right > window.innerWidth) {
popup.style.left = (rect.right - popupRect.width) + 'px';
}
// Adjust vertical position
if (popupRect.bottom > window.innerHeight) {
popup.style.top = (rect.top - popupRect.height - 5) + 'px';
}
}, 0);
// Animate in
requestAnimationFrame(() => {
popup.classList.add('show');
});
}
hidePopup() {
if (this.activePopup) {
this.activePopup.classList.remove('show');
setTimeout(() => {
if (this.activePopup && this.activePopup.parentNode) {
this.activePopup.parentNode.removeChild(this.activePopup);
}
this.activePopup = null;
}, 200);
}
}
formatMetadata(plotName, metadata) {
if (!metadata || Object.keys(metadata).length === 0) {
return `
<div class="metadata-popup-header">
<h4>${plotName}</h4>
</div>
<div class="metadata-popup-content">
<p class="no-metadata">No metadata available</p>
</div>
`;
}
let html = `
<div class="metadata-popup-header">
<h4>${plotName}</h4>
</div>
<div class="metadata-popup-content">
`;
// Show priority fields first
const priorityFields = ['title', 'description', 'plot_type', 'experiment'];
const processedKeys = new Set();
// Display priority fields first
for (const key of priorityFields) {
if (metadata[key] !== undefined) {
html += this.formatMetadataField(key, metadata[key]);
processedKeys.add(key);
}
}
// Show file info if available
if (metadata.file_info) {
html += `<div class="metadata-section-title">File Information</div>`;
html += this.formatMetadataField('File Size', metadata.file_info.size);
if (metadata.file_info.extension) {
html += this.formatMetadataField('Format', metadata.file_info.extension);
}
processedKeys.add('file_info');
}
// Show timestamps if available
if (metadata.timestamps) {
html += `<div class="metadata-section-title">Timestamps</div>`;
if (metadata.timestamps.created_human) {
html += this.formatMetadataField('Created', metadata.timestamps.created_human);
}
if (metadata.timestamps.modified_human) {
html += this.formatMetadataField('Modified', metadata.timestamps.modified_human);
}
processedKeys.add('timestamps');
}
// Show extracted info if available
if (metadata.extracted_info && Object.keys(metadata.extracted_info).length > 0) {
html += `<div class="metadata-section-title">Plot Details</div>`;
for (const [key, value] of Object.entries(metadata.extracted_info)) {
html += this.formatMetadataField(key, value);
}
processedKeys.add('extracted_info');
}
// Display other fields (excluding generation info unless it's the only data)
const otherKeys = Object.keys(metadata).filter(key =>
!processedKeys.has(key) && key !== 'generation'
);
if (otherKeys.length > 0) {
html += `<div class="metadata-section-title">Additional Information</div>`;
for (const key of otherKeys) {
html += this.formatMetadataField(key, metadata[key]);
}
}
// Show generation info last if there's no other meaningful data
if (processedKeys.size <= 2 && metadata.generation) {
html += `<div class="metadata-section-title">Generation Info</div>`;
if (metadata.generation.generation_time) {
const genDate = new Date(metadata.generation.generation_time);
html += this.formatMetadataField('Generated', genDate.toLocaleString());
}
}
html += '</div>';
return html;
}
formatMetadataField(key, value) {
const displayKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
let formattedValue;
if (value === null || value === undefined) {
formattedValue = '<em>null</em>';
} else if (typeof value === 'object') {
if (Array.isArray(value)) {
if (value.length <= 3) {
formattedValue = value.map(item => `<span class="metadata-tag">${item}</span>`).join(' ');
} else {
formattedValue = `${value.slice(0, 3).map(item => `<span class="metadata-tag">${item}</span>`).join(' ')} <span class="metadata-more">+${value.length - 3} more</span>`;
}
} else {
// Show object as compact JSON for small objects, or just key count for large ones
const keys = Object.keys(value);
if (keys.length <= 3) {
formattedValue = '<code>' + JSON.stringify(value) + '</code>';
} else {
formattedValue = `<em>Object with ${keys.length} properties</em>`;
}
}
} else {
// Truncate long strings
const str = String(value);
formattedValue = str.length > 50 ? str.substring(0, 47) + '...' : str;
}
return `
<div class="metadata-field">
<span class="metadata-key">${displayKey}:</span>
<span class="metadata-value">${formattedValue}</span>
</div>
`;
}
}
// Global instance
window.metadataPopup = new MetadataPopup();
// Global function for template usage
window.showMetadataPopup = function(button, plotName, metadata) {
window.metadataPopup.showPopup(button, plotName, metadata);
};
-142
View File
@@ -1,142 +0,0 @@
/**
* Simple Metadata Section Toggle
*
* Handles showing/hiding the metadata grid with a simple button
*/
// Global function to toggle metadata section visibility
function toggleMetadataSection() {
console.log('toggleMetadataSection called');
const content = document.getElementById('metadataContent');
const arrow = document.getElementById('metadataArrow');
if (!content) {
console.log('No metadata content found');
return;
}
const isVisible = content.style.display !== 'none';
if (isVisible) {
// Hide the content
content.style.display = 'none';
if (arrow) arrow.textContent = '▼';
console.log('Metadata hidden');
} else {
// Show the content
content.style.display = 'block';
if (arrow) arrow.textContent = '▲';
console.log('Metadata shown');
// Trigger MathJax rendering for LaTeX content
if (typeof MathJax !== 'undefined') {
MathJax.typesetPromise([content]).catch(function (err) {
console.log('MathJax typeset failed: ' + err.message);
});
}
}
// Save state to localStorage
localStorage.setItem('metadataVisible', (!isVisible).toString());
}
// Function to expand long text
function expandText(button) {
const longText = button.previousElementSibling;
const fullText = button.nextElementSibling;
if (fullText.style.display === 'none') {
longText.style.display = 'none';
fullText.style.display = 'inline';
button.textContent = 'Show less';
} else {
longText.style.display = 'inline';
fullText.style.display = 'none';
button.textContent = 'Show more';
}
}
// Copy metadata file path to clipboard
async function copyMetadataPath() {
const pathElement = document.getElementById('metadata-file-path');
const copyBtn = document.querySelector('.copy-path-btn');
if (!pathElement || !copyBtn) {
console.log('Path element or copy button not found');
return;
}
const path = pathElement.textContent;
console.log('Attempting to copy path:', path);
try {
// Try using the modern clipboard API
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(path);
} else {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = path;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand('copy');
textArea.remove();
}
// Visual feedback
const originalText = copyBtn.innerHTML;
copyBtn.innerHTML = '✅ Copied!';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.innerHTML = originalText;
copyBtn.classList.remove('copied');
}, 2000);
console.log('Path copied successfully');
} catch (err) {
console.error('Failed to copy path: ', err);
// Show error feedback
const originalText = copyBtn.innerHTML;
copyBtn.innerHTML = '❌ Failed';
setTimeout(() => {
copyBtn.innerHTML = originalText;
}, 2000);
}
}
// Make functions globally available
window.toggleMetadataSection = toggleMetadataSection;
window.expandText = expandText;
window.copyMetadataPath = copyMetadataPath;
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
console.log('Metadata section script loaded');
const content = document.getElementById('metadataContent');
if (content) {
// Check if user previously had it expanded
const wasVisible = localStorage.getItem('metadataVisible') === 'true';
if (wasVisible) {
content.style.display = 'block';
const arrow = document.getElementById('metadataArrow');
if (arrow) arrow.textContent = '▲';
} else {
content.style.display = 'none';
const arrow = document.getElementById('metadataArrow');
if (arrow) arrow.textContent = '▼';
}
console.log('Metadata section initialized, visible:', wasVisible);
}
});
-208
View File
@@ -1,208 +0,0 @@
/**
* Navigation functionality - breadcrumbs and folder tree
*/
export class NavigationManager {
/**
* Build breadcrumb navigation based on current path
*/
buildBreadcrumb() {
const currentPath = window.location.pathname;
const pathParts = currentPath.split('/').filter(part => part !== '' && part !== 'index.html');
const breadcrumb = document.getElementById('breadcrumb');
if (!breadcrumb) return;
if (pathParts.length === 0) {
breadcrumb.innerHTML = '<span>🏠 Root</span>';
return;
}
let html = '<a href="/">🏠 Root</a>';
for (let i = 0; i < pathParts.length; i++) {
const part = pathParts[i];
html += '<span class="separator">/</span>';
if (i === pathParts.length - 1) {
html += `<span>${decodeURIComponent(part)}</span>`;
} else {
const levelsUp = pathParts.length - 1 - i;
const relativePath = '../'.repeat(levelsUp) + 'index.html';
html += `<a href="${relativePath}">${decodeURIComponent(part)}</a>`;
}
}
breadcrumb.innerHTML = html;
}
/**
* Build and display the folder tree structure
*/
async buildFolderTree() {
const treeContainer = document.getElementById('folderTree');
const currentPath = window.location.pathname;
if (!treeContainer) return;
try {
const tree = await this.buildTreeRecursive(currentPath, 0, currentPath);
treeContainer.innerHTML = tree;
} catch (error) {
console.error('Error building folder tree:', error);
treeContainer.innerHTML = '<div class="tree-item">❌ Error loading folder tree</div>';
}
}
/**
* Recursively build tree structure for folders with collapsed empty directories
*/
async buildTreeRecursive(path, depth, currentPath, maxDepth = 5) {
if (depth > maxDepth) {
const indent = ' '.repeat(depth);
return `<div class="tree-item">${indent}└─ ...</div>`;
}
try {
const response = await fetch(path);
const htmlContent = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');
const subdirs = doc.querySelectorAll('h2 + ul li a');
const items = doc.querySelectorAll('.grid-item');
// Check if this is an empty directory (only one subdirectory, no items)
if (items.length === 0 && subdirs.length === 1) {
const subdir = subdirs[0];
const href = subdir.getAttribute('href');
if (href) {
const baseUrl = path.replace(/\/[^\/]*$/, '/');
const subPath = baseUrl + href;
const collapsedPath = await this.getCollapsedPath(path, subPath);
return await this.buildCollapsedTreeItem(collapsedPath, depth, currentPath, maxDepth);
}
}
// Normal directory processing
const indent = ' '.repeat(depth);
const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery';
const totalItems = items.length + subdirs.length;
const arrow = depth === 0 ? '' : '└─ ';
let html = '';
if (path === currentPath) {
html += `<div class="tree-item">${indent}${arrow}📁 <span class="tree-current">${folderName}</span> (${totalItems} items)</div>`;
} else {
html += `<div class="tree-item">${indent}${arrow}📁 <a href="${path}" class="tree-link">${folderName}</a> (${totalItems} items)</div>`;
}
for (let i = 0; i < subdirs.length; i++) {
const subdir = subdirs[i];
const href = subdir.getAttribute('href');
if (href) {
const baseUrl = path.replace(/\/[^\/]*$/, '/');
const subPath = baseUrl + href;
html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth);
}
}
return html;
} catch (error) {
const indent = ' '.repeat(depth);
const arrow = depth === 0 ? '' : '└─ ';
const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery';
return `<div class="tree-item">${indent}${arrow}📁 ${folderName} (error loading)</div>`;
}
}
/**
* Get the collapsed path by following empty directories
*/
async getCollapsedPath(startPath, currentPath) {
const pathSegments = [];
let path = startPath;
while (true) {
try {
const response = await fetch(path);
const htmlContent = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');
const subdirs = doc.querySelectorAll('h2 + ul li a');
const items = doc.querySelectorAll('.grid-item');
const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery';
pathSegments.push({ name: folderName, path: path });
if (items.length > 0 || subdirs.length !== 1) {
break;
}
const subdir = subdirs[0];
const href = subdir.getAttribute('href');
if (href) {
const baseUrl = path.replace(/\/[^\/]*$/, '/');
path = baseUrl + href;
} else {
break;
}
} catch (error) {
break;
}
}
return {
segments: pathSegments,
finalPath: path
};
}
/**
* Build a collapsed tree item for empty directory chains
*/
async buildCollapsedTreeItem(collapsedPath, depth, currentPath, maxDepth) {
const indent = ' '.repeat(depth);
const arrow = depth === 0 ? '' : '└─ ';
const displayName = collapsedPath.segments.map(seg => seg.name).join(' / ');
const finalPath = collapsedPath.finalPath;
let totalItems = 0;
let subdirs = [];
try {
const response = await fetch(finalPath);
const htmlContent = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');
const subdirElements = doc.querySelectorAll('h2 + ul li a');
const items = doc.querySelectorAll('.grid-item');
totalItems = items.length + subdirElements.length;
subdirs = Array.from(subdirElements);
} catch (error) {
// Handle error case
}
let html = '';
if (finalPath === currentPath) {
html += `<div class="tree-item">${indent}${arrow}📁 <span class="tree-current">${displayName}</span> (${totalItems} items)</div>`;
} else {
html += `<div class="tree-item">${indent}${arrow}📁 <a href="${finalPath}" class="tree-link">${displayName}</a> (${totalItems} items)</div>`;
}
for (let i = 0; i < subdirs.length; i++) {
const subdir = subdirs[i];
const href = subdir.getAttribute('href');
if (href) {
const baseUrl = finalPath.replace(/\/[^\/]*$/, '/');
const subPath = baseUrl + href;
html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth);
}
}
return html;
}
}
-114
View File
@@ -1,114 +0,0 @@
/**
* Recent plots sidebar management
*/
export class RecentPlotsManager {
constructor(maxRecentPlots = 20) {
this.MAX_RECENT_PLOTS = maxRecentPlots;
this.init();
}
init() {
this.updateRecentPlotsDisplay();
this.trackPlotClicks();
}
/**
* Add plot to recent plots list
*/
addToRecentPlots(plotHref) {
const plotName = plotHref.split('/').pop().replace('.pdf', '');
const pathParts = plotHref.split('/').filter(p => p !== '' && p !== plotName + '.pdf');
const plotPath = pathParts.join(' / ');
const thumbUrl = plotHref.replace('.pdf', '.png');
// Determine the gallery page URL (directory containing the plot)
const plotDir = plotHref.substring(0, plotHref.lastIndexOf('/'));
const galleryUrl = plotDir + '/index.html';
const plotInfo = {
name: plotName,
path: plotPath,
href: plotHref,
thumbUrl: thumbUrl,
galleryUrl: galleryUrl,
timestamp: Date.now()
};
let recentPlots = JSON.parse(localStorage.getItem('recentPlots') || '[]');
recentPlots = recentPlots.filter(p => p.href !== plotHref);
recentPlots.unshift(plotInfo);
recentPlots = recentPlots.slice(0, this.MAX_RECENT_PLOTS);
localStorage.setItem('recentPlots', JSON.stringify(recentPlots));
this.updateRecentPlotsDisplay();
}
/**
* Update recent plots sidebar display
*/
updateRecentPlotsDisplay() {
const sidebarContent = document.getElementById('sidebarContent');
if (!sidebarContent) return;
const recentPlots = JSON.parse(localStorage.getItem('recentPlots') || '[]');
if (recentPlots.length === 0) {
sidebarContent.innerHTML = `
<div style="text-align: center; color: var(--breadcrumb-color); margin: 2rem 0;">
📭 No recent plots yet<br>
<small style="opacity: 0.7;">Open some plots to see them here</small>
</div>
`;
return;
}
let html = '';
recentPlots.forEach(plot => {
html += `
<div class="recent-plot" onclick="window.recentPlotsManager.openRecentPlot('${plot.galleryUrl || plot.href}', '${plot.name}')" title="${plot.name}">
<img src="${plot.thumbUrl}" class="recent-plot-thumb" alt="${plot.name}" />
<div class="recent-plot-info">
<div class="recent-plot-name">${plot.name}</div>
<div class="recent-plot-path">📍 ${plot.path}</div>
</div>
</div>
`;
});
sidebarContent.innerHTML = html;
}
/**
* Open recent plot gallery page and highlight thumbnail
*/
openRecentPlot(galleryUrl, plotName) {
// Navigate to gallery page with plot highlight parameter
const url = new URL(galleryUrl, window.location.origin);
url.searchParams.set('highlight', plotName);
window.location.href = url.toString();
this.toggleSidebar();
}
/**
* Toggle recent plots sidebar
*/
toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
if (sidebar) sidebar.classList.toggle('open');
if (overlay) overlay.classList.toggle('open');
}
/**
* Track clicks on plot links
*/
trackPlotClicks() {
document.addEventListener('click', (e) => {
const link = e.target.closest('a[href$=".pdf"]');
if (link) {
this.addToRecentPlots(link.href);
}
});
}
}
-217
View File
@@ -1,217 +0,0 @@
/**
* 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';
}
}
-197
View File
@@ -1,197 +0,0 @@
/**
* Sort Manager - handles sorting of plot items by name and creation time
*/
export class SortManager {
constructor() {
this.currentSort = 'name';
this.currentOrder = 'asc';
this.init();
}
/**
* Initialize sort controls
*/
init() {
// Use setTimeout to ensure DOM is ready
setTimeout(() => {
this.setupSortButtons();
this.loadSavedSort();
}, 100);
}
/**
* Setup sort button event listeners
*/
setupSortButtons() {
const sortButtons = document.querySelectorAll('.sort-btn');
const orderButton = document.querySelector('.sort-order-btn');
if (sortButtons.length === 0) {
setTimeout(() => this.setupSortButtons(), 500);
return;
}
sortButtons.forEach((button) => {
const sortType = button.getAttribute('data-sort');
button.addEventListener('click', (e) => {
e.preventDefault();
this.setSortType(sortType);
});
});
if (orderButton) {
orderButton.addEventListener('click', (e) => {
e.preventDefault();
this.toggleSortOrder();
});
}
// Initialize button states
this.updateSortButtons();
this.updateOrderButton();
}
/**
* Set the sort type (name or time)
*/
setSortType(sortType) {
if (sortType === this.currentSort) return;
this.currentSort = sortType;
this.updateSortButtons();
this.sortPlots();
this.saveSortPreference();
}
/**
* Toggle sort order between ascending and descending
*/
toggleSortOrder() {
this.currentOrder = this.currentOrder === 'asc' ? 'desc' : 'asc';
this.updateOrderButton();
this.sortPlots();
this.saveSortPreference();
}
/**
* Update visual state of sort buttons
*/
updateSortButtons() {
const sortButtons = document.querySelectorAll('.sort-btn');
sortButtons.forEach(btn => {
if (btn.getAttribute('data-sort') === this.currentSort) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
}
/**
* Update visual state of order button
*/
updateOrderButton() {
const orderButton = document.querySelector('.sort-order-btn');
if (orderButton) {
orderButton.textContent = this.currentOrder === 'asc' ? '↑' : '↓';
orderButton.setAttribute('data-order', this.currentOrder);
orderButton.title = `Sort Order: ${this.currentOrder === 'asc' ? 'Ascending' : 'Descending'}`;
}
}
/**
* Sort the plot items
*/
sortPlots() {
const plotContainer = document.getElementById('plotContainer');
if (!plotContainer) return;
const plotItems = Array.from(plotContainer.children);
plotItems.sort((a, b) => {
let valueA, valueB;
if (this.currentSort === 'name') {
valueA = a.getAttribute('data-name') || '';
valueB = b.getAttribute('data-name') || '';
// Natural sort for better number handling
const result = valueA.localeCompare(valueB, undefined, {
numeric: true,
sensitivity: 'base'
});
return this.currentOrder === 'asc' ? result : -result;
} else if (this.currentSort === 'time') {
valueA = parseInt(a.getAttribute('data-time') || '0');
valueB = parseInt(b.getAttribute('data-time') || '0');
const result = valueA - valueB;
return this.currentOrder === 'asc' ? result : -result;
}
return 0;
});
// Re-append sorted items
plotItems.forEach(item => {
plotContainer.appendChild(item);
});
}
/**
* Save sort preferences to localStorage
*/
saveSortPreference() {
try {
localStorage.setItem('gallery-sort-type', this.currentSort);
localStorage.setItem('gallery-sort-order', this.currentOrder);
} catch (e) {
// Ignore localStorage errors
}
}
/**
* Load saved sort preferences
*/
loadSavedSort() {
try {
const savedSort = localStorage.getItem('gallery-sort-type');
const savedOrder = localStorage.getItem('gallery-sort-order');
if (savedSort && ['name', 'time'].includes(savedSort)) {
this.currentSort = savedSort;
}
if (savedOrder && ['asc', 'desc'].includes(savedOrder)) {
this.currentOrder = savedOrder;
}
this.updateSortButtons();
this.updateOrderButton();
// Sort immediately if there are plots
setTimeout(() => this.sortPlots(), 100);
} catch (e) {
// Ignore localStorage errors, use defaults
}
}
/**
* Get current sort settings
*/
getCurrentSort() {
return {
type: this.currentSort,
order: this.currentOrder
};
}
/**
* Refresh sorting (call this when plot content changes)
*/
refresh() {
this.sortPlots();
}
}
-54
View File
@@ -1,54 +0,0 @@
/**
* Statistics manager for gallery display
*/
export class StatsManager {
constructor() {
this.updateGalleryStats();
}
/**
* Update gallery statistics display
*/
updateGalleryStats() {
// Use stats passed from Python backend if available
// Otherwise fallback to DOM counting
const gridItems = document.querySelectorAll('.grid-item');
const subdirLinks = document.querySelectorAll('a[href$="/index.html"]');
const fileCountEl = document.getElementById('fileCount');
const folderCountEl = document.getElementById('folderCount');
const totalSizeEl = document.getElementById('totalSize');
const lastUpdatedEl = document.getElementById('lastUpdated');
if (fileCountEl) fileCountEl.textContent = gridItems.length;
if (folderCountEl) folderCountEl.textContent = subdirLinks.length;
if (totalSizeEl) totalSizeEl.textContent = 'Unknown';
// Set last updated time
const now = new Date();
const timeStr = now.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
if (lastUpdatedEl) lastUpdatedEl.textContent = timeStr;
}
/**
* Update stats with backend data
*/
updateWithBackendStats(stats) {
const fileCountEl = document.getElementById('fileCount');
const folderCountEl = document.getElementById('folderCount');
const totalSizeEl = document.getElementById('totalSize');
if (fileCountEl && stats.file_count !== undefined) {
fileCountEl.textContent = stats.file_count;
}
if (folderCountEl && stats.folder_count !== undefined) {
folderCountEl.textContent = stats.folder_count;
}
if (totalSizeEl && stats.total_size !== undefined) {
totalSizeEl.textContent = stats.total_size;
}
}
}
-43
View File
@@ -1,43 +0,0 @@
/**
* 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');
}
}
}
-107
View File
@@ -1,107 +0,0 @@
/**
* 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';
}
}
/**
* Toggle keyboard shortcuts help display
*/
static toggleShortcutsHelp() {
const help = document.getElementById('shortcutsHelp');
if (help) {
help.style.display = help.style.display === 'block' ? 'none' : 'block';
}
}
}
-192
View File
@@ -1,192 +0,0 @@
/**
* View Controls Manager - handles switching between grid, list-large, and list-compact views
*/
export class ViewManager {
constructor() {
this.currentView = 'grid';
this.init();
}
/**
* Initialize view controls
*/
init() {
this.setupViewButtons();
this.loadSavedView();
this.updateControlsVisibility();
}
/**
* Update visibility of view controls based on plot content
*/
updateControlsVisibility() {
const plotContainer = document.getElementById('plotContainer');
const controlsContainer = document.querySelector('.controls-container');
if (!plotContainer || !controlsContainer) return;
const hasPlots = plotContainer.children.length > 0;
controlsContainer.style.display = hasPlots ? 'flex' : 'none';
}
/**
* Setup view toggle buttons
*/
setupViewButtons() {
const viewButtons = document.querySelectorAll('.view-btn');
viewButtons.forEach(button => {
button.addEventListener('click', (e) => {
const newView = button.getAttribute('data-view');
this.switchView(newView);
});
});
}
/**
* Switch to a different view mode
*/
switchView(viewMode) {
if (viewMode === this.currentView) return;
const plotContainer = document.getElementById('plotContainer');
const viewButtons = document.querySelectorAll('.view-btn');
if (!plotContainer) return;
// Remove current view class
plotContainer.classList.remove(
'grid-view',
'list-large-view',
'list-compact-view'
);
// Add new view class
switch (viewMode) {
case 'grid':
plotContainer.classList.add('grid-view');
break;
case 'list-large':
plotContainer.classList.add('list-large-view');
break;
case 'list-compact':
plotContainer.classList.add('list-compact-view');
break;
default:
plotContainer.classList.add('grid-view');
viewMode = 'grid';
}
// Update button states
viewButtons.forEach(btn => {
if (btn.getAttribute('data-view') === viewMode) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
// Save the preference
this.currentView = viewMode;
this.saveViewPreference(viewMode);
// Trigger any necessary layout updates
this.onViewChanged(viewMode);
}
/**
* Save view preference to localStorage
*/
saveViewPreference(viewMode) {
try {
localStorage.setItem('gallery-view-mode', viewMode);
} catch (e) {
// Ignore localStorage errors
}
}
/**
* Load saved view preference
*/
loadSavedView() {
try {
const savedView = localStorage.getItem('gallery-view-mode');
if (savedView && ['grid', 'list-large', 'list-compact'].includes(savedView)) {
this.switchView(savedView);
}
} catch (e) {
// Ignore localStorage errors, use default
}
}
/**
* Handle view change events - can be extended for additional functionality
*/
onViewChanged(viewMode) {
// Dispatch custom event for other components that might need to know
const event = new CustomEvent('viewChanged', {
detail: { viewMode }
});
document.dispatchEvent(event);
// Update any other UI elements that depend on view mode
this.updateUIForView(viewMode);
}
/**
* Update UI elements based on current view
*/
updateUIForView(viewMode) {
// You can add view-specific UI updates here
// For example, adjusting search result highlighting, etc.
// Update any tooltips or help text
const viewButtons = document.querySelectorAll('.view-btn');
viewButtons.forEach(btn => {
const btnView = btn.getAttribute('data-view');
if (btnView === viewMode) {
btn.style.transform = 'scale(1.05)';
} else {
btn.style.transform = 'scale(1)';
}
});
}
/**
* Get current view mode
*/
getCurrentView() {
return this.currentView;
}
/**
* Refresh controls visibility (call this when gallery content changes)
*/
refresh() {
this.updateControlsVisibility();
}
/**
* Check if current view is grid mode
*/
isGridView() {
return this.currentView === 'grid';
}
/**
* Check if current view is list mode (either variant)
*/
isListView() {
return this.currentView === 'list-large' || this.currentView === 'list-compact';
}
/**
* Cycle through view modes (useful for keyboard shortcuts)
*/
cycleView() {
const views = ['grid', 'list-large', 'list-compact'];
const currentIndex = views.indexOf(this.currentView);
const nextIndex = (currentIndex + 1) % views.length;
this.switchView(views[nextIndex]);
}
}
+11 -6
View File
@@ -2,7 +2,7 @@
import shutil
from pathlib import Path
from typing import Dict, Any, Optional
from typing import Dict, Any, Optional, Union
from jinja2 import Environment, FileSystemLoader, Template
from gallery.config import GalleryConfig
@@ -22,7 +22,7 @@ from gallery.utils.processing import (
)
def get_template(template_dir: Optional[Path | str] = None):
def get_template(template_dir: Optional[Union[Path, str]] = None):
"""
Get the Jinja2 template for gallery rendering.
@@ -167,11 +167,16 @@ def copy_assets(
gallery_root = Path(config.web_folder) / config.plot_root
assets_dst = gallery_root.parent / "assets"
main_css_src = assets_src / "css" / "main.css"
main_css_dst = assets_dst / "css" / "main.css"
# Use the newest mtime across all source asset files as the staleness check,
# so any change to any JS/CSS file triggers a redeploy.
sentinel_dst = assets_dst / "css" / "main.css"
newest_src_mtime = max(
(f.stat().st_mtime for f in assets_src.rglob('*') if f.is_file()),
default=0,
)
dst_mtime = sentinel_dst.stat().st_mtime if sentinel_dst.exists() else 0
if (not assets_dst.exists() or
needs_update(main_css_src, main_css_dst)):
if not assets_dst.exists() or newest_src_mtime > (dst_mtime + 30):
if assets_dst.exists():
shutil.rmtree(assets_dst)
shutil.copytree(assets_src, assets_dst)
+1 -10
View File
@@ -203,16 +203,6 @@
{% 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">
@@ -357,6 +347,7 @@
stats: {% if stats %}{{ stats|tojson }}{% else %}null{% endif %}
};
</script>
<script id="gallery-data" type="application/json">{"subdirs": {{ subdirs|tojson }}, "item_count": {{ items|length }}}</script>
<!-- Metadata Popup Script -->
<script src="{{ assets_path }}/js/metadata-popup.js"></script>