diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index 47b059e..0000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "flake8.args": [
- "--max-line-length=120",
- "--ignore=W293,E123,W503",
- ],
-}
\ No newline at end of file
diff --git a/README.md b/README.md
index 3203292..1d7c312 100644
--- a/README.md
+++ b/README.md
@@ -38,12 +38,12 @@
## Installation
-### From GitLab
+### From Gitea
Pip install:
```bash
-pip install git+https://gitlab.etp.kit.edu/kschmidt/web
+pip install git+https://git.larsbogner.de/lars/ETPlot
```
Or git clone and `pip install .`. After installation the `gallery` command is available in your shell. Verify with:
@@ -184,7 +184,6 @@ paths:
gallery:
plot_root: "gallery" # subdirectory inside web_folder
png_dpi: 400 # thumbnail resolution
- backup_folder: "" # optional backup path
sources:
- name: "analysis_results"
diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc
deleted file mode 100644
index 9d7e920..0000000
Binary files a/__pycache__/config.cpython-312.pyc and /dev/null differ
diff --git a/assets/css/base.css b/assets/css/base.css
deleted file mode 100644
index 2da21fc..0000000
--- a/assets/css/base.css
+++ /dev/null
@@ -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: 2rem; /* Reduced from 400px - stats box is fixed positioned */
- 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;
- }
-}
diff --git a/assets/css/comparison.css b/assets/css/comparison.css
deleted file mode 100644
index 7e2dee2..0000000
--- a/assets/css/comparison.css
+++ /dev/null
@@ -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;
-}
diff --git a/assets/css/export.css b/assets/css/export.css
deleted file mode 100644
index 84d7b5f..0000000
--- a/assets/css/export.css
+++ /dev/null
@@ -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);
-}
diff --git a/assets/css/floating-elements.css b/assets/css/floating-elements.css
deleted file mode 100644
index cd8e830..0000000
--- a/assets/css/floating-elements.css
+++ /dev/null
@@ -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;
- }
-}
diff --git a/assets/css/folder-metadata.css b/assets/css/folder-metadata.css
deleted file mode 100644
index 8508cf3..0000000
--- a/assets/css/folder-metadata.css
+++ /dev/null
@@ -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;
- }
-}
diff --git a/assets/css/folder-tree.css b/assets/css/folder-tree.css
deleted file mode 100644
index 3c394ad..0000000
--- a/assets/css/folder-tree.css
+++ /dev/null
@@ -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);
-}
diff --git a/assets/css/grid.css b/assets/css/grid.css
deleted file mode 100644
index 8ad9828..0000000
--- a/assets/css/grid.css
+++ /dev/null
@@ -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;
-}
diff --git a/assets/css/main.css b/assets/css/main.css
deleted file mode 100644
index c6019d7..0000000
--- a/assets/css/main.css
+++ /dev/null
@@ -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');
diff --git a/assets/css/metadata-section.css b/assets/css/metadata-section.css
deleted file mode 100644
index 7988467..0000000
--- a/assets/css/metadata-section.css
+++ /dev/null
@@ -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;
- }
-}
diff --git a/assets/css/metadata.css b/assets/css/metadata.css
deleted file mode 100644
index 872fef6..0000000
--- a/assets/css/metadata.css
+++ /dev/null
@@ -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;
- }
-}
diff --git a/assets/css/navigation.css b/assets/css/navigation.css
deleted file mode 100644
index 1dc5629..0000000
--- a/assets/css/navigation.css
+++ /dev/null
@@ -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;
-}
diff --git a/assets/css/responsive.css b/assets/css/responsive.css
deleted file mode 100644
index 1ca81f5..0000000
--- a/assets/css/responsive.css
+++ /dev/null
@@ -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;
- }
-}
diff --git a/assets/css/search.css b/assets/css/search.css
deleted file mode 100644
index 7bd6995..0000000
--- a/assets/css/search.css
+++ /dev/null
@@ -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;
-}
diff --git a/assets/css/sidebar.css b/assets/css/sidebar.css
deleted file mode 100644
index 7718fb1..0000000
--- a/assets/css/sidebar.css
+++ /dev/null
@@ -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;
-}
diff --git a/assets/css/sort-controls.css b/assets/css/sort-controls.css
deleted file mode 100644
index 3df2439..0000000
--- a/assets/css/sort-controls.css
+++ /dev/null
@@ -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;
-}
diff --git a/assets/css/sort-debug.css b/assets/css/sort-debug.css
deleted file mode 100644
index e69de29..0000000
diff --git a/assets/css/stats.css b/assets/css/stats.css
deleted file mode 100644
index 10868e5..0000000
--- a/assets/css/stats.css
+++ /dev/null
@@ -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;
- }
-}
diff --git a/assets/css/variables.css b/assets/css/variables.css
deleted file mode 100644
index 72c531e..0000000
--- a/assets/css/variables.css
+++ /dev/null
@@ -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;
-}
diff --git a/assets/css/view-controls.css b/assets/css/view-controls.css
deleted file mode 100644
index 8e7a9b9..0000000
--- a/assets/css/view-controls.css
+++ /dev/null
@@ -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: 2rem; /* Reduced from 450px - stats box is fixed positioned */
- 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;
- }
-}
diff --git a/assets/css/view-override.css b/assets/css/view-override.css
deleted file mode 100644
index e9bd47c..0000000
--- a/assets/css/view-override.css
+++ /dev/null
@@ -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;
-}
diff --git a/assets/js/comparison-manager.js b/assets/js/comparison-manager.js
deleted file mode 100644
index 7056775..0000000
--- a/assets/js/comparison-manager.js
+++ /dev/null
@@ -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 = `
-
-
- ${plotInfo.name}
- ${plotInfo.path}
-
- `;
- }
-
- if (title) title.textContent = plotInfo.name;
- if (replaceBtn) replaceBtn.style.display = 'block';
-
- // Reset grid item styles
- const gridItems = document.querySelectorAll('.grid-item');
- gridItems.forEach(item => {
- item.style.cursor = '';
- item.style.border = '';
- });
- }
-
- /**
- * Replace plot in comparison
- */
- replacePlot(slot) {
- this.selectPlotForComparison(slot);
- }
-}
diff --git a/assets/js/export-manager.js b/assets/js/export-manager.js
deleted file mode 100644
index b60967b..0000000
--- a/assets/js/export-manager.js
+++ /dev/null
@@ -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 = `
-
- ☐
-
- `;
- 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 = `
-
-
🚀 Export Selected Plots
-
Run the following command in your terminal to export the selected plots:
-
-
-
- ${fullCommand}
-
-
-
- 📋 Copy Command
-
-
- 📄 Copy JSON
-
-
- ✕ Close
-
-
-
-
-
-
📋 Command Breakdown:
-
- Creates temporary file: ${tempFilePath}
- Changes to work directory: ${workDir}
- Runs export script: python export_plots.py
- Output file: Will be saved in the work directory
-
-
-
-
-
💡 Tips:
-
- The temporary JSON file will be automatically cleaned up after successful export
- Use Esc to exit selection mode
- Press Ctrl+E to toggle selection mode
-
-
-
- `;
-
- 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();
- });
- }
-});
diff --git a/assets/js/folder-metadata.js b/assets/js/folder-metadata.js
deleted file mode 100644
index 86f9db4..0000000
--- a/assets/js/folder-metadata.js
+++ /dev/null
@@ -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');
- }
- });
-});
diff --git a/assets/js/gallery-app.js b/assets/js/gallery-app.js
deleted file mode 100644
index 849286e..0000000
--- a/assets/js/gallery-app.js
+++ /dev/null
@@ -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); }
-}
diff --git a/assets/js/keyboard-manager.js b/assets/js/keyboard-manager.js
deleted file mode 100644
index c535a9c..0000000
--- a/assets/js/keyboard-manager.js
+++ /dev/null
@@ -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';
- }
-}
diff --git a/assets/js/main.js b/assets/js/main.js
deleted file mode 100644
index 8f924e6..0000000
--- a/assets/js/main.js
+++ /dev/null
@@ -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;
-});
diff --git a/assets/js/metadata-popup.js b/assets/js/metadata-popup.js
deleted file mode 100644
index 085c3a9..0000000
--- a/assets/js/metadata-popup.js
+++ /dev/null
@@ -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 `
-
-
- `;
- }
-
- let html = `
-
- ';
- 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 = 'null ';
- } else if (typeof value === 'object') {
- if (Array.isArray(value)) {
- if (value.length <= 3) {
- formattedValue = value.map(item => `${item} `).join(' ');
- } else {
- formattedValue = `${value.slice(0, 3).map(item => `${item} `).join(' ')} +${value.length - 3} more `;
- }
- } 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 = '' + JSON.stringify(value) + '';
- } else {
- formattedValue = `Object with ${keys.length} properties `;
- }
- }
- } else {
- // Truncate long strings
- const str = String(value);
- formattedValue = str.length > 50 ? str.substring(0, 47) + '...' : str;
- }
-
- return `
-
- ${displayKey}:
- ${formattedValue}
-
- `;
- }
-}
-
-// Global instance
-window.metadataPopup = new MetadataPopup();
-
-// Global function for template usage
-window.showMetadataPopup = function(button, plotName, metadata) {
- window.metadataPopup.showPopup(button, plotName, metadata);
-};
diff --git a/assets/js/metadata-section.js b/assets/js/metadata-section.js
deleted file mode 100644
index b8f2485..0000000
--- a/assets/js/metadata-section.js
+++ /dev/null
@@ -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);
- }
-});
diff --git a/assets/js/navigation-manager.js b/assets/js/navigation-manager.js
deleted file mode 100644
index b9a69d6..0000000
--- a/assets/js/navigation-manager.js
+++ /dev/null
@@ -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 = '🏠 Root ';
- return;
- }
-
- let html = '🏠 Root ';
-
- for (let i = 0; i < pathParts.length; i++) {
- const part = pathParts[i];
- html += '/ ';
-
- if (i === pathParts.length - 1) {
- html += `${decodeURIComponent(part)} `;
- } else {
- const levelsUp = pathParts.length - 1 - i;
- const relativePath = '../'.repeat(levelsUp) + 'index.html';
- html += `${decodeURIComponent(part)} `;
- }
- }
-
- breadcrumb.innerHTML = html;
- }
-
- /**
- * Build and display the folder tree structure
- */
- async buildFolderTree() {
- const treeContainer = document.getElementById('folderTree');
- const currentPath = window.location.pathname;
-
- if (!treeContainer) return;
-
- try {
- const tree = await this.buildTreeRecursive(currentPath, 0, currentPath);
- treeContainer.innerHTML = tree;
- } catch (error) {
- console.error('Error building folder tree:', error);
- treeContainer.innerHTML = '❌ Error loading folder tree
';
- }
- }
-
- /**
- * Recursively build tree structure for folders with collapsed empty directories
- */
- async buildTreeRecursive(path, depth, currentPath, maxDepth = 5) {
- if (depth > maxDepth) {
- const indent = ' '.repeat(depth);
- return `${indent}└─ ...
`;
- }
-
- try {
- const response = await fetch(path);
- const htmlContent = await response.text();
- const parser = new DOMParser();
- const doc = parser.parseFromString(htmlContent, 'text/html');
-
- const subdirs = doc.querySelectorAll('h2 + ul li a');
- const items = doc.querySelectorAll('.grid-item');
-
- // Check if this is an empty directory (only one subdirectory, no items)
- if (items.length === 0 && subdirs.length === 1) {
- const subdir = subdirs[0];
- const href = subdir.getAttribute('href');
- if (href) {
- const baseUrl = path.replace(/\/[^\/]*$/, '/');
- const subPath = baseUrl + href;
-
- const collapsedPath = await this.getCollapsedPath(path, subPath);
- return await this.buildCollapsedTreeItem(collapsedPath, depth, currentPath, maxDepth);
- }
- }
-
- // Normal directory processing
- const indent = ' '.repeat(depth);
- const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery';
- const totalItems = items.length + subdirs.length;
- const arrow = depth === 0 ? '' : '└─ ';
-
- let html = '';
- if (path === currentPath) {
- html += `${indent}${arrow}📁 ${folderName} (${totalItems} items)
`;
- } else {
- html += ``;
- }
-
- for (let i = 0; i < subdirs.length; i++) {
- const subdir = subdirs[i];
- const href = subdir.getAttribute('href');
- if (href) {
- const baseUrl = path.replace(/\/[^\/]*$/, '/');
- const subPath = baseUrl + href;
- html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth);
- }
- }
-
- return html;
- } catch (error) {
- const indent = ' '.repeat(depth);
- const arrow = depth === 0 ? '' : '└─ ';
- const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery';
- return `${indent}${arrow}📁 ${folderName} (error loading)
`;
- }
- }
-
- /**
- * Get the collapsed path by following empty directories
- */
- async getCollapsedPath(startPath, currentPath) {
- const pathSegments = [];
- let path = startPath;
-
- while (true) {
- try {
- const response = await fetch(path);
- const htmlContent = await response.text();
- const parser = new DOMParser();
- const doc = parser.parseFromString(htmlContent, 'text/html');
-
- const subdirs = doc.querySelectorAll('h2 + ul li a');
- const items = doc.querySelectorAll('.grid-item');
-
- const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery';
- pathSegments.push({ name: folderName, path: path });
-
- if (items.length > 0 || subdirs.length !== 1) {
- break;
- }
-
- const subdir = subdirs[0];
- const href = subdir.getAttribute('href');
- if (href) {
- const baseUrl = path.replace(/\/[^\/]*$/, '/');
- path = baseUrl + href;
- } else {
- break;
- }
- } catch (error) {
- break;
- }
- }
-
- return {
- segments: pathSegments,
- finalPath: path
- };
- }
-
- /**
- * Build a collapsed tree item for empty directory chains
- */
- async buildCollapsedTreeItem(collapsedPath, depth, currentPath, maxDepth) {
- const indent = ' '.repeat(depth);
- const arrow = depth === 0 ? '' : '└─ ';
-
- const displayName = collapsedPath.segments.map(seg => seg.name).join(' / ');
- const finalPath = collapsedPath.finalPath;
-
- let totalItems = 0;
- let subdirs = [];
- try {
- const response = await fetch(finalPath);
- const htmlContent = await response.text();
- const parser = new DOMParser();
- const doc = parser.parseFromString(htmlContent, 'text/html');
-
- const subdirElements = doc.querySelectorAll('h2 + ul li a');
- const items = doc.querySelectorAll('.grid-item');
- totalItems = items.length + subdirElements.length;
-
- subdirs = Array.from(subdirElements);
- } catch (error) {
- // Handle error case
- }
-
- let html = '';
- if (finalPath === currentPath) {
- html += `${indent}${arrow}📁 ${displayName} (${totalItems} items)
`;
- } else {
- html += ``;
- }
-
- for (let i = 0; i < subdirs.length; i++) {
- const subdir = subdirs[i];
- const href = subdir.getAttribute('href');
- if (href) {
- const baseUrl = finalPath.replace(/\/[^\/]*$/, '/');
- const subPath = baseUrl + href;
- html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth);
- }
- }
-
- return html;
- }
-}
diff --git a/assets/js/recent-plots-manager.js b/assets/js/recent-plots-manager.js
deleted file mode 100644
index 29fd3bc..0000000
--- a/assets/js/recent-plots-manager.js
+++ /dev/null
@@ -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 = `
-
- 📭 No recent plots yet
- Open some plots to see them here
-
- `;
- return;
- }
-
- let html = '';
- recentPlots.forEach(plot => {
- html += `
-
-
-
-
${plot.name}
-
📍 ${plot.path}
-
-
- `;
- });
-
- sidebarContent.innerHTML = html;
- }
-
- /**
- * Open recent plot gallery page and highlight thumbnail
- */
- openRecentPlot(galleryUrl, plotName) {
- // Navigate to gallery page with plot highlight parameter
- const url = new URL(galleryUrl, window.location.origin);
- url.searchParams.set('highlight', plotName);
- window.location.href = url.toString();
- this.toggleSidebar();
- }
-
- /**
- * Toggle recent plots sidebar
- */
- toggleSidebar() {
- const sidebar = document.getElementById('sidebar');
- const overlay = document.getElementById('sidebarOverlay');
-
- if (sidebar) sidebar.classList.toggle('open');
- if (overlay) overlay.classList.toggle('open');
- }
-
- /**
- * Track clicks on plot links
- */
- trackPlotClicks() {
- document.addEventListener('click', (e) => {
- const link = e.target.closest('a[href$=".pdf"]');
- if (link) {
- this.addToRecentPlots(link.href);
- }
- });
- }
-}
diff --git a/assets/js/search-manager.js b/assets/js/search-manager.js
deleted file mode 100644
index 20eff08..0000000
--- a/assets/js/search-manager.js
+++ /dev/null
@@ -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 = '🔍 Searching...
';
- searchResults.style.display = 'block';
-
- try {
- const results = await this.searchPlots(query);
- this.displaySearchResults(results, query);
- } catch (error) {
- console.error('Search error:', error);
- searchResults.innerHTML = '❌ Search failed
';
- }
- }
-
- /**
- * Search for plots matching the query
- */
- async searchPlots(query) {
- const results = [];
- const visited = new Set();
- const lowerQuery = query.toLowerCase();
-
- await this.searchInPage(window.location.pathname, lowerQuery, results, visited);
- await this.searchRecursive(window.location.pathname, lowerQuery, results, visited, 0, 5);
-
- return results.slice(0, 20);
- }
-
- /**
- * Search for plots in a specific page
- */
- async searchInPage(path, query, results, visited, maxResults = 50) {
- if (visited.has(path) || results.length >= maxResults) return;
- visited.add(path);
-
- try {
- const response = await fetch(path);
- const html = await response.text();
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, 'text/html');
-
- const items = doc.querySelectorAll('.grid-item');
- items.forEach(item => {
- const nameElement = item.querySelector('.plot-name');
- const imgElement = item.querySelector('img');
- const linkElement = item.querySelector('a');
-
- if (nameElement && imgElement && linkElement) {
- const name = nameElement.textContent.toLowerCase();
- if (name.includes(query)) {
- results.push({
- name: nameElement.textContent,
- path: path,
- href: linkElement.href,
- imgSrc: imgElement.src,
- relevance: this.calculateRelevance(name, query)
- });
- }
- }
- });
- } catch (error) {
- console.error('Error searching in', path, error);
- }
- }
-
- /**
- * Recursively search in subdirectories
- */
- async searchRecursive(path, query, results, visited, depth, maxDepth) {
- if (depth >= maxDepth || results.length >= 50) return;
-
- try {
- const response = await fetch(path);
- const html = await response.text();
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, 'text/html');
-
- const subdirs = doc.querySelectorAll('h2 + ul li a');
- for (const subdir of subdirs) {
- const href = subdir.getAttribute('href');
- if (href) {
- const baseUrl = path.replace(/\/[^\/]*$/, '/');
- const subPath = baseUrl + href;
- await this.searchInPage(subPath, query, results, visited);
- await this.searchRecursive(subPath, query, results, visited, depth + 1, maxDepth);
- }
- }
- } catch (error) {
- console.error('Error in recursive search:', error);
- }
- }
-
- /**
- * Calculate search relevance score
- */
- calculateRelevance(text, query) {
- const exactMatch = text === query;
- const startsWith = text.startsWith(query);
- const wordMatch = text.split(/\s+/).some(word => word.startsWith(query));
-
- if (exactMatch) return 100;
- if (startsWith) return 80;
- if (wordMatch) return 60;
- return 40;
- }
-
- /**
- * Display search results with highlighting
- */
- displaySearchResults(results, query) {
- const searchResults = document.getElementById('searchResults');
- if (!searchResults) return;
-
- if (results.length === 0) {
- searchResults.innerHTML = '📭 No plots found
';
- return;
- }
-
- results.sort((a, b) => b.relevance - a.relevance);
-
- let html = '';
- results.forEach(result => {
- const highlightedName = this.highlightText(result.name, query);
- const relativePath = this.getRelativePath(result.path);
-
- html += `
-
-
-
-
-
- ${highlightedName}
-
-
- 📍 ${relativePath}
-
-
-
-
- `;
- });
-
- searchResults.innerHTML = html;
- }
-
- /**
- * Highlight search query in text
- */
- highlightText(text, query) {
- const regex = new RegExp(`(${query})`, 'gi');
- return text.replace(regex, '$1 ');
- }
-
- /**
- * Get relative path for display
- */
- getRelativePath(fullPath) {
- const parts = fullPath.split('/').filter(p => p !== '' && p !== 'index.html');
- return parts.length > 0 ? parts.join(' / ') : 'Root';
- }
-
- /**
- * Open search result and track it
- */
- openSearchResult(href) {
- if (window.recentPlotsManager) {
- window.recentPlotsManager.addToRecentPlots(href);
- }
- window.open(href, '_blank');
- document.getElementById('searchResults').style.display = 'none';
- }
-}
diff --git a/assets/js/sort-manager.js b/assets/js/sort-manager.js
deleted file mode 100644
index 8ec9a85..0000000
--- a/assets/js/sort-manager.js
+++ /dev/null
@@ -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();
- }
-}
diff --git a/assets/js/stats-manager.js b/assets/js/stats-manager.js
deleted file mode 100644
index 9ad8df5..0000000
--- a/assets/js/stats-manager.js
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/assets/js/theme-manager.js b/assets/js/theme-manager.js
deleted file mode 100644
index f23affc..0000000
--- a/assets/js/theme-manager.js
+++ /dev/null
@@ -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');
- }
- }
-}
diff --git a/assets/js/utils.js b/assets/js/utils.js
deleted file mode 100644
index 3c5b432..0000000
--- a/assets/js/utils.js
+++ /dev/null
@@ -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';
- }
- }
-}
diff --git a/assets/js/view-manager.js b/assets/js/view-manager.js
deleted file mode 100644
index 0d51208..0000000
--- a/assets/js/view-manager.js
+++ /dev/null
@@ -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]);
- }
-}
diff --git a/config.lbogner.yaml b/config.lbogner.yaml
index ca41126..52243b3 100644
--- a/config.lbogner.yaml
+++ b/config.lbogner.yaml
@@ -14,7 +14,6 @@ paths:
gallery:
plot_root: "gallery"
png_dpi: 400
- backup_folder: ""
ui:
max_recent_plots: 20
diff --git a/config.yaml b/config.yaml
index b30ed9c..cd990fd 100644
--- a/config.yaml
+++ b/config.yaml
@@ -17,9 +17,6 @@ gallery:
# PNG conversion quality
png_dpi: 400
- # Backup folder (leave empty to disable)
- backup_folder: ""
-
# UI Settings
ui:
# Maximum number of recent plots to track
diff --git a/docs/COVERAGE_TESTING.md b/docs/COVERAGE_TESTING.md
deleted file mode 100644
index e834301..0000000
--- a/docs/COVERAGE_TESTING.md
+++ /dev/null
@@ -1,275 +0,0 @@
-# Automated Coverage Testing Documentation
-
-## Overview
-
-This repository now includes automated code coverage testing using the `coverage.py` package. Coverage testing helps ensure that your tests adequately exercise your codebase and identifies untested code paths.
-
-## 🚀 Quick Start
-
-### Container-based Coverage (Recommended)
-```bash
-# Build and test with coverage in container
-./tests/test_container.sh
-
-# Or run coverage directly in container
-apptainer exec gallery-generator.sif python3 /src/tests/run_coverage.py
-```
-
-### Local Coverage Testing
-```bash
-# Run coverage tests locally
-./tests/run_coverage_local.sh
-
-# Or manually
-pip install coverage
-coverage run -m unittest tests.test_container
-coverage report
-coverage html
-```
-
-## 📁 Coverage Files
-
-### Core Coverage Files
-- **`.coveragerc`** - Coverage configuration file
-- **`tests/run_coverage.py`** - Automated coverage script for containers
-- **`tests/run_coverage_local.sh`** - Local coverage testing script
-
-### Generated Reports
-- **`coverage.xml`** - XML format for CI/CD integration
-- **`coverage_html_report/`** - Interactive HTML reports
-- **`.coverage`** - Coverage data file
-
-## 🔧 Configuration
-
-### Coverage Settings (`.coveragerc`)
-```ini
-[run]
-source = .
-omit =
- tests/* # Exclude test files
- __pycache__/* # Exclude cache
- assets/* # Exclude static assets
- docs/* # Exclude documentation
- templates/* # Exclude templates
-
-[report]
-precision = 2 # 2 decimal places
-show_missing = True # Show missing line numbers
-skip_covered = False # Show all files
-
-[html]
-directory = coverage_html_report
-title = Gallery Generator Coverage Report
-```
-
-### Singularity Container Integration
-The coverage package is automatically installed in the container:
-```bash
-pip install --no-cache-dir jinja2 pyyaml coverage
-```
-
-## 📊 Coverage Reports
-
-### Console Report
-Shows coverage percentage and missing lines:
-```
-Name Stmts Miss Cover Missing
------------------------------------------------------
-generate_gallery.py 190 45 76.32% 156-167, 234-245
-orchestration/config.py 45 8 82.22% 78-82
-orchestration/logger.py 67 12 82.09% 45-48, 89-94
------------------------------------------------------
-TOTAL 302 65 78.48%
-```
-
-### HTML Report
-Interactive report with:
-- Line-by-line coverage highlighting
-- Branch coverage details
-- Sortable file listings
-- Coverage trends
-
-### XML Report
-Machine-readable format for CI/CD:
-- GitLab CI coverage visualization
-- External tool integration
-- Coverage badges
-
-## 🎯 Coverage Targets
-
-### Current Thresholds
-- **Minimum Target**: 80% overall coverage
-- **Warning Level**: Below 70% coverage
-- **Exclusions**: Test files, static assets, documentation
-
-### Best Practices
-- **Focus on Core Logic**: Prioritize business logic coverage
-- **Test Edge Cases**: Include error handling and boundary conditions
-- **Regular Monitoring**: Run coverage with every commit
-- **Incremental Improvement**: Gradually increase coverage over time
-
-## 🔄 CI/CD Integration
-
-### GitLab CI Pipeline
-The coverage testing is integrated into the GitLab CI pipeline:
-
-```yaml
-test:coverage:
- stage: test
- script:
- - apptainer exec $CONTAINER_IMAGE python3 /src/tests/run_coverage.py
- coverage: '/TOTAL.+?(\d+\.\d+)%/'
- artifacts:
- reports:
- coverage_report:
- coverage_format: cobertura
- path: coverage.xml
-```
-
-### Features
-- **Automatic Reports**: Coverage reports in merge requests
-- **Badge Integration**: Coverage badges in README
-- **Trend Tracking**: Historical coverage data
-- **Failure Thresholds**: Fail builds below minimum coverage
-
-## 🛠️ Advanced Usage
-
-### Custom Coverage Runs
-```bash
-# Test specific modules
-coverage run --source=orchestration -m unittest tests.test_metadata
-
-# Include/exclude patterns
-coverage run --omit="*/tests/*" -m unittest discover
-
-# Branch coverage (more detailed)
-coverage run --branch -m unittest tests.test_container
-```
-
-### Coverage Analysis
-```bash
-# Show missing lines
-coverage report --show-missing
-
-# Generate detailed HTML
-coverage html --show-contexts
-
-# Export data
-coverage json
-coverage xml
-```
-
-### Integration with IDEs
-- **VS Code**: Coverage Gutters extension
-- **PyCharm**: Built-in coverage runner
-- **Vim**: Coverage highlighting plugins
-
-## 📈 Coverage Metrics
-
-### What Coverage Measures
-- **Statement Coverage**: Lines of code executed
-- **Branch Coverage**: Decision paths taken
-- **Function Coverage**: Functions called
-- **Class Coverage**: Classes instantiated
-
-### What Coverage Doesn't Measure
-- **Code Quality**: Coverage ≠ good tests
-- **Logic Correctness**: 100% coverage ≠ bug-free
-- **Performance**: Execution speed not measured
-- **Security**: Vulnerabilities not detected
-
-## 🧪 Testing Strategy
-
-### Container Test Suite Coverage
-Current test files and their focus:
-
-#### `tests/test_container.py`
-- **Environment validation** - Container setup
-- **Utility functions** - Helper functions
-- **Metadata system** - YAML processing
-- **PDF processing** - ImageMagick integration
-- **Gallery generation** - End-to-end workflow
-
-#### `tests/test_build_container.py`
-- **Container building** - Singularity build process
-- **Dependency validation** - Package installation
-- **Application functionality** - Script execution
-
-### Coverage Gaps Analysis
-Use `tests/test_coverage.py` to analyze:
-- Missing function coverage
-- Untested code paths
-- Critical functionality gaps
-- Integration test needs
-
-## 🚨 Troubleshooting
-
-### Common Issues
-
-#### No Coverage Data
-```bash
-# Ensure coverage is running tests
-coverage run --debug=trace -m unittest tests.test_container
-```
-
-#### Import Errors
-```bash
-# Check PYTHONPATH
-export PYTHONPATH=/src:$PYTHONPATH
-```
-
-#### Permission Issues
-```bash
-# Container write permissions
-apptainer exec --writable-tmpfs container.sif python3 tests/run_coverage.py
-```
-
-### Debug Commands
-```bash
-# Check coverage configuration
-coverage debug config
-
-# Verify data collection
-coverage debug data
-
-# Test discovery
-coverage debug sys
-```
-
-## 📚 References
-
-- **Coverage.py Documentation**: https://coverage.readthedocs.io/
-- **GitLab CI Coverage**: https://docs.gitlab.com/ee/ci/testing/code_coverage.html
-- **Testing Best Practices**: Python Testing 101
-- **Container Testing**: Singularity/Apptainer Documentation
-
-## 🔄 Maintenance
-
-### Regular Tasks
-- **Weekly**: Review coverage reports
-- **Monthly**: Update coverage targets
-- **Release**: Ensure minimum coverage met
-- **Quarterly**: Review exclusion patterns
-
-### Cleanup
-```bash
-# Remove coverage files
-./tests/cleanup.sh
-
-# Manual cleanup
-rm -f .coverage coverage.xml
-rm -rf coverage_html_report/
-```
-
-### Updates
-```bash
-# Update coverage package
-pip install --upgrade coverage
-
-# Update container
-apptainer build --force container.sif Singularity.def
-```
-
----
-
-*This automated coverage system provides comprehensive testing insights while maintaining the containerized, dependency-free approach of the gallery generator project.*
diff --git a/docs/SORT_IMPLEMENTATION.md b/docs/SORT_IMPLEMENTATION.md
deleted file mode 100644
index b0dae4a..0000000
--- a/docs/SORT_IMPLEMENTATION.md
+++ /dev/null
@@ -1,136 +0,0 @@
-# Gallery Sort Functionality Implementation Guide
-
-## Overview
-This guide explains how to integrate the new sorting functionality that allows users to sort plots by name and creation time.
-
-## Frontend Implementation (Complete ✅)
-
-The frontend implementation is complete and includes:
-
-### 1. Sort Controls UI
-- **Name/Time buttons**: Toggle between sorting by filename and creation time
-- **Order button**: Toggle between ascending (↑) and descending (↓) order
-- **Positioned**: Left side of the controls container, next to view toggle buttons
-- **Responsive**: Adapts to mobile layouts
-
-### 2. Keyboard Shortcuts
-- `Ctrl+N`: Sort by name
-- `Ctrl+M`: Sort by time (modification/creation time)
-- `Ctrl+O`: Toggle sort order (ascending/descending)
-
-### 3. Persistence
-- Sort preferences are saved to localStorage
-- Settings persist across page reloads and navigation
-
-### 4. Tile Sizing
-- Grid view now shows ~6 plots per row on desktop (240px minimum width)
-- Responsive design maintains usability on mobile devices
-
-## Backend Integration (Required)
-
-To enable time-based sorting, you need to modify your Python gallery generation code:
-
-### 1. Add Creation Time to Plot Items
-
-```python
-from pathlib import Path
-
-def add_creation_time_to_items(items, base_path):
- """Add creation time to plot items for sorting functionality."""
- for item in items:
- try:
- # Get creation time from PNG or PDF file
- png_path = None
- pdf_path = None
-
- if 'png_href' in item:
- png_rel_path = item['png_href'].replace('../', '').replace('./', '')
- png_path = Path(base_path) / png_rel_path
-
- if 'pdf_href' in item:
- pdf_rel_path = item['pdf_href'].replace('../', '').replace('./', '')
- pdf_path = Path(base_path) / pdf_rel_path
-
- # Use PNG creation time if available, otherwise PDF
- creation_time = 0
- if png_path and png_path.exists():
- creation_time = int(png_path.stat().st_ctime)
- elif pdf_path and pdf_path.exists():
- creation_time = int(pdf_path.stat().st_ctime)
-
- item['creation_time'] = creation_time
-
- except Exception as e:
- print(f"Warning: Could not get creation time for {item.get('name', 'unknown')}: {e}")
- item['creation_time'] = 0
-
- return items
-```
-
-### 2. Integrate into Your Gallery Generation
-
-In your existing gallery generation code, call this function before rendering the template:
-
-```python
-# Your existing code that creates the items list
-items = generate_plot_items() # Your existing function
-
-# Add creation times
-items = add_creation_time_to_items(items, gallery_base_path)
-
-# Pass to template
-template.render(items=items, ...)
-```
-
-### 3. Template Data Structure
-
-The template now expects each item to have a `creation_time` field:
-
-```python
-item = {
- 'name': 'plot_name.png',
- 'png_href': './plot_name.png',
- 'pdf_href': './plot_name.pdf',
- 'creation_time': 1642723200 # Unix timestamp
-}
-```
-
-## File Locations
-
-### Frontend Files (Ready to use)
-- `templates/gallery.html` - Updated with sort controls and data attributes
-- `assets/css/view-controls.css` - Styling for sort and view controls
-- `assets/css/view-override.css` - Grid layout with larger tiles
-- `assets/js/sort-manager.js` - Sort functionality implementation
-- `assets/js/gallery-app.js` - Integration of SortManager
-- `assets/js/keyboard-manager.js` - Keyboard shortcuts for sorting
-
-### Backend Integration
-- `python/add_creation_time.py` - Example implementation for adding creation times
-
-## Features Summary
-
-### ✅ Completed Features
-1. **Larger Grid Tiles**: ~6 plots per row instead of 8
-2. **Sort Controls**: Name and time sorting with visual feedback
-3. **Sort Order Toggle**: Ascending/descending with visual indicator
-4. **Keyboard Shortcuts**: Quick access to all sort functions
-5. **Persistence**: Settings saved across sessions
-6. **Responsive Design**: Works on all screen sizes
-7. **Template Integration**: Data attributes ready for backend
-
-### 🔄 Next Steps (Backend Integration)
-1. Modify your Python gallery generation code to include `creation_time`
-2. Use the provided `add_creation_time_to_items()` function
-3. Test with real plot files to ensure timestamps are correct
-
-## Testing
-
-After backend integration:
-1. Navigate to a gallery with multiple plots
-2. Click the sort buttons to verify functionality
-3. Use keyboard shortcuts to test responsiveness
-4. Check that sort order toggles correctly
-5. Verify settings persist after page reload
-
-The frontend is fully functional and will work immediately once the backend provides the `creation_time` data.
diff --git a/gallery/assets/css/export.css b/gallery/assets/css/export.css
deleted file mode 100644
index 84d7b5f..0000000
--- a/gallery/assets/css/export.css
+++ /dev/null
@@ -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);
-}
diff --git a/gallery/assets/css/main.css b/gallery/assets/css/main.css
index c6019d7..b9dfb44 100644
--- a/gallery/assets/css/main.css
+++ b/gallery/assets/css/main.css
@@ -18,7 +18,6 @@
@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');
diff --git a/gallery/assets/css/sort-debug.css b/gallery/assets/css/sort-debug.css
deleted file mode 100644
index e69de29..0000000
diff --git a/gallery/assets/js/export-manager.js b/gallery/assets/js/export-manager.js
deleted file mode 100644
index ab221c3..0000000
--- a/gallery/assets/js/export-manager.js
+++ /dev/null
@@ -1,444 +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 = `
-
- ☐
-
- `;
- 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`;
-
- // 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);
- }
-
- /**
- * 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) {
- const tempFilePath = `/tmp/${tempFileName}`;
- const fullCommand = `echo '${requestData.replace(/'/g, "'\\''")}' > ${tempFilePath} && python export_plots.py ${tempFilePath}`;
-
- const instructions = `
-
-
🚀 Export Selected Plots
-
Run the following command in your terminal to export the selected plots:
-
-
-
- ${fullCommand}
-
-
-
- 📋 Copy Command
-
-
- 📄 Copy JSON
-
-
- ✕ Close
-
-
-
-
-
-
📋 Command Breakdown:
-
- Creates temporary file: ${tempFilePath}
- Runs export script: python export_plots.py
- Output file: Will be saved in the work directory
-
-
-
-
-
💡 Tips:
-
- The temporary JSON file will be automatically cleaned up after successful export
- Use Esc to exit selection mode
- Press Ctrl+E to toggle selection mode
-
-
-
- `;
-
- 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();
- });
- }
-});
diff --git a/gallery/config/__init__.py b/gallery/config/__init__.py
index 44f5426..54840b8 100644
--- a/gallery/config/__init__.py
+++ b/gallery/config/__init__.py
@@ -167,7 +167,6 @@ class GalleryConfig:
plot_root: str = GalleryDefaults.plot_root
cache_enabled: bool = GalleryDefaults.cache_enabled
inherit_from_parent: bool = GalleryDefaults.inherit_from_parent
- backup_folder: str = ""
def __post_init__(self):
if isinstance(self.web_folder, str):
@@ -215,7 +214,6 @@ class GalleryConfig:
plot_root=gallery_cfg.get("plot_root", GalleryDefaults.plot_root),
cache_enabled=metadata_cfg.get("cache_enabled", GalleryDefaults.cache_enabled),
inherit_from_parent=metadata_cfg.get("inherit_from_parent", GalleryDefaults.inherit_from_parent),
- backup_folder=gallery_cfg.get("backup_folder", ""),
)
def to_yaml(self, yaml_file: Union[str, Path]) -> None:
@@ -229,7 +227,6 @@ class GalleryConfig:
"gallery": {
"plot_root": self.plot_root,
"png_dpi": self.png_dpi,
- "backup_folder": self.backup_folder,
},
"ui": {
"max_recent_plots": 20,
diff --git a/gallery/config/config.yaml b/gallery/config/config.yaml
index 833c266..04f80a0 100644
--- a/gallery/config/config.yaml
+++ b/gallery/config/config.yaml
@@ -1,5 +1,4 @@
gallery:
- backup_folder: ''
plot_root: gallery
png_dpi: 400
metadata:
diff --git a/gallery/tui.py b/gallery/tui.py
index d236611..7169208 100644
--- a/gallery/tui.py
+++ b/gallery/tui.py
@@ -41,7 +41,6 @@ CONFIG_FIELDS = [
("web-folder", "paths.web_folder", True),
("plot-root", "gallery.plot_root", False),
("png-dpi", "gallery.png_dpi", False),
- ("backup-folder", "gallery.backup_folder", False),
("cache-enabled", "metadata.cache_enabled", False),
("inherit-meta", "metadata.inherit_from_parent", False),
]
@@ -250,9 +249,6 @@ class GalleryTUI(App):
with Horizontal(classes="field-row"):
yield Label("PNG DPI")
yield Input(id="png-dpi", placeholder="400")
- with Horizontal(classes="field-row"):
- yield Label("Backup folder")
- yield Input(id="backup-folder", placeholder="leave empty to disable")
with Horizontal(classes="field-row"):
yield Label("Cache metadata")
yield Input(id="cache-enabled", placeholder="true")
diff --git a/gallery/utils/backup.py b/gallery/utils/backup.py
deleted file mode 100644
index 14ab225..0000000
--- a/gallery/utils/backup.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""Backup utilities for gallery."""
-
-import datetime
-import zipfile
-from pathlib import Path
-
-
-def create_backup(web_folder: Path, backup_folder: Path) -> bool:
- """
- Create a backup of the web folder.
-
- Args:
- web_folder: Path to the web folder to backup
- backup_folder: Path to the backup directory
-
- Returns:
- True if backup was created successfully, False otherwise
- """
- try:
- today = datetime.date.today().strftime("%Y%m%d")
- backup_name = f"backup-{today}.zip"
- backup_path = backup_folder / backup_name
-
- backup_folder.mkdir(parents=True, exist_ok=True)
-
- if backup_path.exists():
- return True
-
- with zipfile.ZipFile(backup_path, "w", zipfile.ZIP_DEFLATED) as zipf:
- for path in web_folder.rglob("*"):
- if path.is_file():
- arcname = path.relative_to(web_folder.parent)
- zipf.write(path, arcname)
- return True
- except Exception as e:
- print(f"Warning: Could not create backup: {e}")
- return False
diff --git a/generate_gallery.py b/generate_gallery.py
deleted file mode 100644
index 15873b7..0000000
--- a/generate_gallery.py
+++ /dev/null
@@ -1,489 +0,0 @@
-"""
-Scientific Gallery Generator
-
-This module generates static HTML galleries from PDF plots.
-It converts PDF plots to PNG thumbnails, creates responsive web interfaces,
-and organizes plots into hierarchical directory structures.
-
-Features:
-- PDF to PNG conversion with configurable DPI
-- Incremental updates (only converts when source is newer)
-- Jinja2 templating for consistent HTML generation
-- Support for nested folder structures
-- Responsive grid layout with search and navigation
-"""
-
-import subprocess
-import shutil
-import os
-import sys
-import argparse
-import time
-from pathlib import Path
-from typing import Dict, Any, Optional
-from datetime import datetime
-from jinja2 import Environment, FileSystemLoader
-
-from orchestration.config import Config
-from orchestration.logger import GalleryLogger, create_logger
-from orchestration.metadata import (
- load_folder_metadata,
- merge_metadata,
- resolve_metadata_for_plot,
- save_metadata_cache,
- get_metadata_file_path
-)
-
-
-CONFIG = Config.from_yaml("config.yaml")
-
-
-def datetime_from_timestamp(timestamp):
- """Convert a Unix timestamp to a datetime object."""
- return datetime.fromtimestamp(timestamp)
-
-
-def strftime_filter(dt, fmt):
- """Format a datetime object using strftime."""
- return dt.strftime(fmt)
-
-
-env = Environment(loader=FileSystemLoader("."))
-env.filters['datetime_from_timestamp'] = datetime_from_timestamp
-env.filters['strftime'] = strftime_filter
-template = env.get_template("templates/gallery.html")
-
-
-def convert_pdf_to_png(pdf_path: Path, logger: GalleryLogger) -> None:
- """
- Convert a PDF file to PNG format using ImageMagick.
-
- Only converts if the PNG doesn't exist or if the PDF is newer than
- the PNG (with a 30-second buffer to handle filesystem timing issues).
-
- Args:
- pdf_path: Path to the source PDF file
-
- Raises:
- subprocess.CalledProcessError: If ImageMagick conversion fails
- """
- png_path = pdf_path.with_suffix(".png")
-
- if png_path.exists():
- pdf_mtime = pdf_path.stat().st_mtime
- png_mtime = png_path.stat().st_mtime
- if png_mtime >= (pdf_mtime + 30):
- logger.skipped_pdf(pdf_path.name)
- return
- else:
- logger.debug(f"PDF {pdf_path.name} is newer than PNG, reconverting...")
-
- # Perform conversion
- start_time = time.time()
- logger.debug(f"Converting {pdf_path} → {png_path}")
-
- try:
- subprocess.run([
- "convert",
- "-density", str(CONFIG.png_dpi),
- str(pdf_path),
- "-quality", "95",
- str(png_path)
- ], check=True)
-
- duration = time.time() - start_time
- logger.converted_pdf(pdf_path.name, duration)
-
- except subprocess.CalledProcessError as e:
- logger.error(f"Failed to convert {pdf_path.name}: {e}")
- raise
-
-
-def needs_update(source_file: Path, target_file: Path) -> bool:
- """
- Check if target file needs updating based on source modification time.
-
- Args:
- source_file: Path to the source file
- target_file: Path to the target file
-
- Returns:
- True if target needs update, False otherwise
- """
- if not target_file.exists():
- return True
-
- source_mtime = source_file.stat().st_mtime
- target_mtime = target_file.stat().st_mtime
-
- return source_mtime > (target_mtime + 30)
-
-
-def build_gallery(source_dir: Path, web_dir: Path,
- relative_path: Path = None,
- inherited_metadata: Optional[Dict[str, Any]] = None,
- logger: Optional[GalleryLogger] = None) -> None:
- """
- Recursively build gallery structure from source directory.
-
- Processes all PDF files in the source directory, converts them to PNG,
- copies both to the web directory, and generates index.html files with
- navigation and thumbnails. Now includes metadata support.
-
- Args:
- source_dir: Source directory containing PDF files
- web_dir: Target web directory for gallery output
- relative_path: Relative path from gallery root (for navigation)
- inherited_metadata: Metadata inherited from parent directories
- """
- if relative_path is None:
- relative_path = Path(".")
-
- if inherited_metadata is None:
- inherited_metadata = {}
-
- if logger is None:
- logger = GalleryLogger(verbose=False)
-
- # Load folder-level metadata and merge with inherited metadata
- folder_metadata = load_folder_metadata(source_dir)
- current_metadata = merge_metadata(inherited_metadata, folder_metadata)
-
- # Log metadata discovery
- if folder_metadata:
- metadata_file_path = get_metadata_file_path(source_dir)
- metadata_path = Path(metadata_file_path)
- if metadata_path.exists():
- logger.found_metadata(metadata_path.name, len(folder_metadata))
-
- pdf_files = list(source_dir.glob("*.pdf"))
- subdirs = [d for d in source_dir.iterdir() if d.is_dir()]
-
- # Log directory discovery
- if pdf_files:
- logger.found_directory(source_dir.name if source_dir.name else "root", len(pdf_files))
-
- # Log individual PDF discovery in verbose mode
- for pdf_file in pdf_files:
- logger.found_pdf(pdf_file.name)
-
- items = []
- plot_metadata_cache = {}
-
- for pdf_file in pdf_files:
- png_file = pdf_file.with_suffix(".png")
-
- web_pdf = web_dir / pdf_file.name
- web_png = web_dir / png_file.name
-
- # Copy PDF if needed
- if needs_update(pdf_file, web_pdf):
- logger.debug(f"Copying {pdf_file} to {web_pdf}")
- shutil.copy2(pdf_file, web_pdf)
- else:
- logger.debug(f"Skipping {pdf_file.name} (PDF up to date)")
-
- # Convert PDF to PNG if needed
- if not png_file.exists():
- convert_pdf_to_png(pdf_file, logger)
-
- if needs_update(png_file, web_png):
- logger.debug(f"Copying {png_file} to {web_png}")
- shutil.copy2(png_file, web_png)
- else:
- logger.debug(f"Skipping {png_file.name} (PNG up to date)")
-
- # Resolve metadata for this specific plot
- plot_metadata = resolve_metadata_for_plot(pdf_file, current_metadata)
- plot_metadata_cache[pdf_file.stem] = plot_metadata
-
- # Get source file creation time (in seconds since epoch)
- source_creation_time = int(pdf_file.stat().st_ctime)
-
- items.append({
- "name": pdf_file.stem,
- "pdf_href": pdf_file.name,
- "png_href": png_file.name,
- "metadata": plot_metadata,
- "creation_time": source_creation_time
- })
-
- # Save metadata cache for this directory
- save_metadata_cache(web_dir, plot_metadata_cache)
-
- subdir_names = []
- for subdir in subdirs:
- subdir_web = web_dir / subdir.name
- subdir_web.mkdir(exist_ok=True)
- subdir_relative = relative_path / subdir.name
- # Pass logger and current metadata to subdirectories
- build_gallery(subdir, subdir_web, subdir_relative, current_metadata, logger)
- subdir_names.append(subdir.name)
-
- output_html = web_dir / "index.html"
-
- # Always regenerate HTML to ensure subdirectory changes are reflected
- # This ensures new subdirectories appear in navigation
- force_regeneration = True
- if output_html.exists() and not force_regeneration:
- html_mtime = output_html.stat().st_mtime
- # Check if any subdirectory is newer than the HTML file
- for subdir in subdirs:
- if subdir.stat().st_mtime > html_mtime:
- logger.debug(f"Subdirectory {subdir.name} is newer, forcing regeneration")
- break
-
- if relative_path == Path("."):
- title = "Gallery"
- else:
- title = f"Gallery: {relative_path}"
-
- # Calculate statistics for current directory
- current_stats = calculate_directory_stats(web_dir)
- stats = {
- "file_count": len(items),
- "folder_count": len(subdir_names),
- "total_size": format_file_size(current_stats["total_size"]),
- "total_size_bytes": current_stats["total_size"]
- }
-
- # Calculate relative path to assets based on directory depth
- if relative_path == Path("."):
- assets_path = "../assets"
- else:
- # Count directory levels to go back to gallery, then to public_html
- depth = len(relative_path.parts)
- assets_path = "../" * (depth + 1) + "assets"
-
- with output_html.open("w") as f:
- rendered_html = template.render(
- title=title,
- items=items,
- subdirs=subdir_names,
- relpath=str(relative_path),
- paths=CONFIG.paths,
- ui=CONFIG.ui,
- stats=stats,
- folder_metadata=current_metadata,
- assets_path=assets_path,
- source_dir=str(source_dir),
- metadata_file_path=get_metadata_file_path(source_dir)
- )
- f.write(rendered_html)
-
- logger.generated_html(str(output_html), len(items))
-
-
-def calculate_directory_stats(directory: Path) -> dict:
- """
- Calculate statistics for a directory.
-
- Args:
- directory: Path to the directory to analyze
-
- Returns:
- Dictionary containing file count, folder count, and total size
- """
- stats = {
- "file_count": 0,
- "folder_count": 0,
- "total_size": 0,
- "pdf_size": 0,
- "png_size": 0
- }
-
- if not directory.exists():
- return stats
-
- for item in directory.rglob("*"):
- if item.is_file():
- stats["file_count"] += 1
- size = item.stat().st_size
- stats["total_size"] += size
-
- if item.suffix.lower() == '.pdf':
- stats["pdf_size"] += size
- elif item.suffix.lower() == '.png':
- stats["png_size"] += size
- elif item.is_dir():
- stats["folder_count"] += 1
-
- return stats
-
-
-def format_file_size(size_bytes: int) -> str:
- """
- Format file size in human readable format.
-
- Args:
- size_bytes: Size in bytes
-
- Returns:
- Formatted size string
- """
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB"]
- size = float(size_bytes)
- i = 0
- while size >= 1024 and i < len(size_names) - 1:
- size /= 1024
- i += 1
-
- return f"{size:.1f} {size_names[i]}"
-
-
-def refresh_gallery_cgi():
- """
- CGI handler to refresh the gallery from a web request.
- Outputs a minimal HTTP response and triggers gallery regeneration.
- """
- import traceback
- print("Content-Type: text/plain\n")
- try:
- main()
- print("Gallery refreshed successfully.")
- except Exception as e:
- print(f"Error refreshing gallery: {e}")
- traceback.print_exc(file=sys.stdout)
-
-
-def main(clean_first: bool = False, verbose: bool = False) -> None:
- """
- Main entry point for gallery generation.
-
- Args:
- clean_first: If True, removes and recreates the gallery directory
- verbose: If True, enables verbose logging
-
- Processes all configured sources and generates the complete gallery
- structure in the web directory. Ensures assets are available.
- """
- # Initialize the logger with appropriate verbosity
- logger = GalleryLogger(verbose=verbose)
-
- gallery_root = Path(CONFIG.web_folder) / CONFIG.plot_root
-
- if clean_first and gallery_root.exists():
- logger.info(f"Cleaning gallery directory {gallery_root}...")
- shutil.rmtree(gallery_root)
-
- # Ensure gallery root exists
- gallery_root.mkdir(parents=True, exist_ok=True)
-
- # Always ensure assets are up to date
- assets_src = Path("assets")
- assets_dst = gallery_root.parent / "assets"
-
- if assets_src.exists():
- # Update assets if they don't exist or are outdated
- main_css_src = assets_src / "css" / "main.css"
- main_css_dst = assets_dst / "css" / "main.css"
- if not assets_dst.exists() or needs_update(main_css_src, main_css_dst):
- if assets_dst.exists():
- shutil.rmtree(assets_dst)
- shutil.copytree(assets_src, assets_dst)
- logger.assets_updated()
- else:
- logger.warning(f"Assets directory {assets_src} not found")
-
- for source in CONFIG.sources:
- source_path = Path(source.path)
-
- if source_path.is_file() and source_path.suffix == '.pdf':
- source_web_dir = gallery_root / source.name
- source_web_dir.mkdir(parents=True, exist_ok=True)
-
- pdf_name = source_path.name
- png_name = source_path.with_suffix('.png').name
-
- web_pdf_path = source_web_dir / pdf_name
- web_png_path = source_web_dir / png_name
- source_png_path = source_path.with_suffix('.png')
-
- if needs_update(source_path, web_pdf_path):
- logger.debug(f"Copying {source_path} to {web_pdf_path}")
- shutil.copy2(source_path, web_pdf_path)
- else:
- logger.debug(f"Skipping {source_path.name} (up to date)")
-
- if not source_png_path.exists():
- convert_pdf_to_png(source_path, logger)
-
- if needs_update(source_png_path, web_png_path):
- logger.debug(f"Copying {source_png_path} to {web_png_path}")
- shutil.copy2(source_png_path, web_png_path)
- else:
- logger.debug(f"Skipping {source_png_path.name} (up to date)")
-
- # Get source file creation time for single file
- source_creation_time = int(source_path.stat().st_ctime)
-
- items = [{
- "name": source_path.stem,
- "pdf_href": pdf_name,
- "png_href": png_name,
- "creation_time": source_creation_time
- }]
-
- # Calculate statistics for single file
- current_stats = calculate_directory_stats(source_web_dir)
- stats = {
- "file_count": 1,
- "folder_count": 0,
- "total_size": format_file_size(current_stats["total_size"]),
- "total_size_bytes": current_stats["total_size"]
- }
-
- # Calculate relative path to assets for single file
- # Single files are at depth 1 (gallery_root/source.name/index.html)
- assets_path = "../assets"
-
- output_html = source_web_dir / "index.html"
- with output_html.open("w") as f:
- f.write(template.render(
- title=source.name,
- items=items,
- subdirs=[],
- relpath=source.name,
- paths=CONFIG.paths,
- ui=CONFIG.ui,
- stats=stats,
- folder_metadata={},
- assets_path=assets_path,
- source_dir=str(source_path.parent),
- metadata_file_path=get_metadata_file_path(source_path.parent)
- ))
-
- logger.generated_html(str(output_html), 1)
- logger.info(f"✓ Completed {source.name}")
-
- elif source_path.is_dir():
- source_web_dir = gallery_root / source.name
- source_web_dir.mkdir(parents=True, exist_ok=True)
- build_gallery(source_path, source_web_dir, Path(source.name), {}, logger)
- logger.info(f"✓ Completed {source.name}")
- else:
- logger.warning(f"Source {source.path} is neither a directory nor a PDF file")
-
- logger.summary(len(CONFIG.sources))
-
-
-if __name__ == "__main__":
- if 'GATEWAY_INTERFACE' in os.environ:
- refresh_gallery_cgi()
- else:
- parser = argparse.ArgumentParser(description='Generate gallery')
- parser.add_argument(
- '--clean',
- action='store_true',
- help='Clean gallery directory before generation'
- )
- parser.add_argument(
- '--verbose', '-v',
- action='store_true',
- help='Enable verbose logging'
- )
- args = parser.parse_args()
- main(clean_first=args.clean, verbose=args.verbose)
diff --git a/orchestration/backup.py b/orchestration/backup.py
deleted file mode 100644
index d1937a4..0000000
--- a/orchestration/backup.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import zipfile
-import datetime
-from pathlib import Path
-
-WEB_FOLDER = Path("plots")
-BACKUP_FOLDER = Path("backups")
-
-today = datetime.date.today().strftime("%Y%m%d")
-backup_name = f"backup-{today}.zip"
-backup_path = BACKUP_FOLDER / backup_name
-
-BACKUP_FOLDER.mkdir(parents=True, exist_ok=True)
-
-if backup_path.exists():
- print(f"Backup already exists: {backup_path}")
-else:
- print(f"Creating backup: {backup_path}")
- with zipfile.ZipFile(backup_path, "w", zipfile.ZIP_DEFLATED) as zipf:
- for path in WEB_FOLDER.rglob("*"):
- if path.is_file():
- arcname = path.relative_to(WEB_FOLDER.parent)
- zipf.write(path, arcname)
- print("✅ Backup complete.")
diff --git a/orchestration/config.py b/orchestration/config.py
deleted file mode 100644
index 9672315..0000000
--- a/orchestration/config.py
+++ /dev/null
@@ -1,147 +0,0 @@
-"""
-Scientific Gallery Configuration Management
-
-This module provides dataclasses and utilities for managing configuration
-of the scientific gallery system, including paths, gallery settings,
-UI preferences, and data sources.
-"""
-
-from dataclasses import dataclass, field, asdict
-from pathlib import Path
-import yaml
-
-
-@dataclass
-class PathConfig:
- """Configuration for system paths and directories."""
- work_dir: str
- web_folder: str
- cgi_script: str
- config_path: str
-
-
-@dataclass
-class GalleryConfig:
- """Configuration for gallery generation and display settings."""
- plot_root: str
- png_dpi: int
- backup_folder: str
-
-
-@dataclass
-class UIConfig:
- """Configuration for user interface behavior and preferences."""
- max_recent_plots: int
- search_debounce_ms: int
-
-
-@dataclass
-class MetadataConfig:
- """Configuration for metadata handling."""
- cache_enabled: bool = True
- inherit_from_parent: bool = True
- supported_formats: list[str] = field(
- default_factory=lambda: ['.yaml', '.yml', '.json']
- )
-
-
-@dataclass
-class GalleryItem:
- """Represents a single data source for the gallery."""
- name: str
- path: Path
-
-
-@dataclass
-class Config:
- """
- Main configuration class that aggregates all gallery settings.
-
- Provides backward compatibility properties and methods for loading
- configuration from YAML files.
- """
- paths: PathConfig
- gallery: GalleryConfig
- ui: UIConfig
- metadata: MetadataConfig
- sources: list[GalleryItem] = field(default_factory=list)
-
- @property
- def web_folder(self):
- """Backward compatibility property for web folder path."""
- return self.paths.web_folder
-
- @property
- def png_dpi(self):
- """Backward compatibility property for PNG conversion DPI."""
- return self.gallery.png_dpi
-
- @property
- def plot_root(self):
- """Backward compatibility property for plot root directory."""
- return self.gallery.plot_root
-
- @property
- def backup_folder(self):
- """Backward compatibility property for backup folder path."""
- return self.gallery.backup_folder
-
- @classmethod
- def from_yaml(cls, yaml_file: str) -> "Config":
- """
- Load configuration from a YAML file.
-
- Args:
- yaml_file: Path to the YAML configuration file
-
- Returns:
- Config instance with loaded settings
-
- Raises:
- FileNotFoundError: If the YAML file doesn't exist
- yaml.YAMLError: If the YAML file is malformed
- """
- with open(yaml_file, "r") as f:
- data = yaml.safe_load(f)
-
- paths_data = data.get('paths', {})
- gallery_data = data.get('gallery', {})
- ui_data = data.get('ui', {})
- metadata_data = data.get('metadata', {})
- sources_data = data.get('sources', [])
-
- paths = PathConfig(**paths_data)
- gallery = GalleryConfig(**gallery_data)
- ui = UIConfig(**ui_data)
- metadata = MetadataConfig(**metadata_data)
-
- sources = [
- GalleryItem(name=source["name"], path=Path(source["path"]))
- for source in sources_data
- ]
-
- return cls(
- paths=paths,
- gallery=gallery,
- ui=ui,
- metadata=metadata,
- sources=sources
- )
-
- def to_yaml(self, yaml_file: str) -> None:
- """
- Save the current configuration to a YAML file.
-
- Args:
- yaml_file: Path where to save the YAML configuration
-
- Raises:
- IOError: If unable to write to the specified file
- """
- with open(yaml_file, "w") as f:
- yaml.dump(asdict(self), f, default_flow_style=False)
-
-
-if __name__ == "__main__":
- config = Config.from_yaml("config.yaml")
- print("Loaded config successfully:", config)
diff --git a/orchestration/logger.py b/orchestration/logger.py
deleted file mode 100644
index 8e6de4f..0000000
--- a/orchestration/logger.py
+++ /dev/null
@@ -1,250 +0,0 @@
-"""
-Gallery logging wrapper using Python's built-in logging module with tree-like output.
-"""
-
-import logging
-import sys
-
-
-class Colors:
- """ANSI color codes for terminal output"""
- RESET = '\033[0m'
- BOLD = '\033[1m'
-
- # Standard colors
- RED = '\033[31m'
- GREEN = '\033[32m'
- YELLOW = '\033[33m'
- BLUE = '\033[34m'
- MAGENTA = '\033[35m'
- CYAN = '\033[36m'
- WHITE = '\033[37m'
-
- # Bright colors
- BRIGHT_GREEN = '\033[92m'
- BRIGHT_YELLOW = '\033[93m'
- BRIGHT_BLUE = '\033[94m'
- BRIGHT_CYAN = '\033[96m'
-
-
-class TreeFormatter(logging.Formatter):
- """Custom formatter that creates clean output with colors"""
-
- def __init__(self, use_colors: bool = True):
- super().__init__()
- self.use_colors = use_colors and sys.stdout.isatty()
-
- def _colorize(self, text: str, color: str) -> str:
- """Apply color to text if colors are enabled"""
- if not self.use_colors:
- return text
- return f"{color}{text}{Colors.RESET}"
-
- def format(self, record):
- # Extract custom attributes from the record
- indent = getattr(record, 'indent', 0)
-
- # Create simple indentation
- prefix = " " * indent
-
- # Apply colors based on level and content
- message = record.getMessage()
-
- if record.levelname == 'INFO':
- if 'Processing:' in message:
- # Main source headers
- message = self._colorize(message, Colors.BOLD + Colors.MAGENTA)
- elif 'Generated' in message:
- message = self._colorize(message, Colors.GREEN)
- elif 'PDF files' in message:
- message = self._colorize(message, Colors.BLUE)
- elif 'Converting' in message:
- message = self._colorize(message, Colors.CYAN)
- elif 'Completed' in message and 'sources' in message:
- message = self._colorize(message, Colors.BOLD + Colors.GREEN)
- else:
- message = self._colorize(message, Colors.WHITE)
- elif record.levelname == 'WARNING':
- message = self._colorize(f"WARNING: {message}", Colors.YELLOW)
- elif record.levelname == 'ERROR':
- message = self._colorize(f"ERROR: {message}", Colors.RED)
- elif record.levelname == 'DEBUG':
- message = self._colorize(message, Colors.CYAN)
- else:
- message = message
-
- return f"{prefix}{message}"
-
-
-class GalleryLogger:
- """
- Simple logger for gallery generation using Python's logging module.
- Provides convenient methods for common logging patterns with clean output.
- """
-
- def __init__(self, name: str = "gallery", level: int = logging.INFO,
- verbose: bool = False, use_colors: bool = True):
- """
- Initialize the gallery logger.
-
- Args:
- name: Logger name
- level: Logging level (default: INFO)
- verbose: If True, enables DEBUG level logging
- use_colors: If True, enables colored output
- """
- self.logger = logging.getLogger(name)
- self.use_colors = use_colors
- self.current_source = None
- self.current_depth = 0
-
- # Set level based on verbose flag
- if verbose:
- self.logger.setLevel(logging.DEBUG)
- else:
- self.logger.setLevel(level)
-
- # Only add handler if logger doesn't have one already
- if not self.logger.handlers:
- handler = logging.StreamHandler(sys.stdout)
- formatter = TreeFormatter(use_colors=use_colors)
- handler.setFormatter(formatter)
- self.logger.addHandler(handler)
-
- # Prevent propagation to avoid duplicate messages
- self.logger.propagate = False
-
- def _log_with_tree(self, level: int, message: str, indent: int = 0):
- """Log a message with simple indentation"""
- # Store indentation info in a way the formatter can access
- original_makeRecord = self.logger.makeRecord
-
- def makeRecord_with_indent(*args, **kwargs):
- record = original_makeRecord(*args, **kwargs)
- record.indent = indent
- return record
-
- # Temporarily replace makeRecord to add our custom attributes
- self.logger.makeRecord = makeRecord_with_indent
- try:
- self.logger.log(level, message)
- finally:
- # Restore original makeRecord
- self.logger.makeRecord = original_makeRecord
-
- def start_source(self, source_name: str):
- """Begin processing a new source"""
- self.current_source = source_name
- self.current_depth = 0
- if self.use_colors:
- colored_name = f"{Colors.BOLD}{Colors.MAGENTA}{source_name}{Colors.RESET}"
- self.logger.info(f"\nProcessing: {colored_name}")
- else:
- self.logger.info(f"\nProcessing: {source_name}")
-
- def info(self, message: str, indent: int = 0):
- """Log an info message"""
- self._log_with_tree(logging.INFO, message, indent)
-
- def debug(self, message: str, indent: int = 0):
- """Log a debug message"""
- self._log_with_tree(logging.DEBUG, message, indent)
-
- def warning(self, message: str, indent: int = 0):
- """Log a warning message"""
- self._log_with_tree(logging.WARNING, message, indent)
-
- def error(self, message: str, indent: int = 0):
- """Log an error message"""
- self._log_with_tree(logging.ERROR, message, indent)
-
- def found_directory(self, dir_name: str, pdf_count: int, indent: int = 1):
- """Log discovery of a directory with PDFs"""
- if pdf_count > 0:
- self._log_with_tree(
- logging.INFO,
- f"Found {dir_name}/ ({pdf_count} PDFs)",
- indent
- )
-
- def found_pdf(self, pdf_name: str, indent: int = 2):
- """Log discovery of a PDF file (debug only)"""
- self._log_with_tree(logging.DEBUG, f"Found PDF: {pdf_name}", indent)
-
- def found_metadata(self, metadata_file: str, field_count: int, indent: int = 1):
- """Log discovery of a metadata file"""
- self._log_with_tree(
- logging.INFO,
- f"Found metadata: {metadata_file} ({field_count} fields)",
- indent
- )
-
- def converted_pdf(self, pdf_name: str, duration: float = None, indent: int = 2):
- """Log successful PDF conversion"""
- if duration is not None:
- self._log_with_tree(
- logging.INFO,
- f"Converting {pdf_name} ({duration:.2f}s)",
- indent
- )
- else:
- self._log_with_tree(logging.INFO, f"Converting {pdf_name}", indent)
-
- def skipped_pdf(self, pdf_name: str, reason: str = "up-to-date", indent: int = 2):
- """Log skipped PDF conversion"""
- self._log_with_tree(logging.DEBUG, f"Skipping {pdf_name} ({reason})", indent)
-
- def generated_html(self, html_path: str, plot_count: int, indent: int = 1):
- """Log HTML page generation"""
- # Extract just the meaningful part of the path
- if '/gallery/' in html_path:
- short_path = html_path.split('/gallery/')[-1]
- else:
- short_path = html_path
-
- if plot_count > 0:
- self._log_with_tree(
- logging.INFO,
- f"Generated {short_path} ({plot_count} plots)",
- indent
- )
- else:
- self._log_with_tree(
- logging.DEBUG,
- f"Generated {short_path} (index only)",
- indent
- )
-
- def assets_updated(self):
- """Log assets update"""
- self.logger.info("Assets updated")
-
- def summary(self, source_count: int):
- """Print a summary"""
- if self.use_colors:
- message = f"{Colors.BOLD}{Colors.GREEN}Completed processing {source_count} sources{Colors.RESET}"
- else:
- message = f"Completed processing {source_count} sources"
- self.logger.info(f"\n{message}")
-
-
-def create_logger(verbose: bool = False, quiet: bool = False, use_colors: bool = True) -> GalleryLogger:
- """
- Create a configured logger for the gallery application.
-
- Args:
- verbose: Enable debug-level logging
- quiet: Suppress most output (only errors and warnings)
- use_colors: Enable colored output
-
- Returns:
- Configured GalleryLogger instance
- """
- if quiet:
- level = logging.WARNING
- elif verbose:
- level = logging.DEBUG
- else:
- level = logging.INFO
-
- return GalleryLogger(level=level, verbose=verbose, use_colors=use_colors)
diff --git a/orchestration/metadata.py b/orchestration/metadata.py
deleted file mode 100644
index deb13d3..0000000
--- a/orchestration/metadata.py
+++ /dev/null
@@ -1,159 +0,0 @@
-"""
-Metadata Management for Scientific Gallery Generator
-
-This module handles loading, parsing, and caching of metadata for plots
-and folders in the gallery system. Supports YAML and JSON formats with
-hierarchical inheritance.
-
-Features:
-- Load metadata from YAML/JSON files
-- Hierarchical metadata inheritance from parent folders
-- Plot-specific metadata overrides
-- Metadata caching for performance
-"""
-
-import json
-import yaml
-import logging
-from pathlib import Path
-from typing import Dict, Any
-
-
-def load_metadata_file(metadata_path: Path) -> Dict[str, Any]:
- """
- Load metadata from a YAML or JSON file.
-
- Args:
- metadata_path: Path to the metadata file
-
- Returns:
- Dictionary containing the metadata, empty dict if file doesn't exist
- or can't be parsed
- """
- if not metadata_path.exists():
- return {}
-
- try:
- with metadata_path.open('r', encoding='utf-8') as f:
- suffix_lower = metadata_path.suffix.lower()
- if suffix_lower == '.yaml' or suffix_lower == '.yml':
- return yaml.safe_load(f) or {}
- elif metadata_path.suffix.lower() == '.json':
- return json.load(f) or {}
- else:
- logging.warning(f"Unknown metadata file format: {metadata_path}")
- return {}
- except (yaml.YAMLError, json.JSONDecodeError, IOError) as e:
- logging.warning(f"Could not parse metadata file {metadata_path}: {e}")
- raise e
-
-
-def load_folder_metadata(folder_path: Path) -> Dict[str, Any]:
- """
- Load folder-level metadata from metadata.yaml, metadata.yml, or metadata.json.
-
- Args:
- folder_path: Path to the folder to check for metadata
-
- Returns:
- Dictionary containing the folder metadata
- """
- # Try YAML first, then JSON for backwards compatibility
- for filename in ['metadata.yaml', 'metadata.yml', 'metadata.json']:
- metadata_path = folder_path / filename
- if metadata_path.exists():
- return load_metadata_file(metadata_path)
-
- return {}
-
-
-def get_metadata_file_path(folder_path: Path) -> str:
- """
- Get the metadata file path for a folder. Returns existing file if found,
- otherwise suggests metadata.yaml (preferred format).
-
- Args:
- folder_path: Path to the folder to check for metadata
-
- Returns:
- String path to the metadata file (existing or suggested)
- """
- # Preferred order: YAML first, then JSON
- preferred_files = ['metadata.yaml', 'metadata.yml', 'metadata.json']
-
- for filename in preferred_files:
- metadata_path = folder_path / filename
- if metadata_path.exists():
- return str(metadata_path)
-
- # If no file exists, suggest metadata.yaml (preferred format)
- return str(folder_path / 'metadata.yaml')
-
-
-def merge_metadata(
- parent_metadata: Dict[str, Any],
- child_metadata: Dict[str, Any]
-) -> Dict[str, Any]:
- """
- Merge parent and child metadata, with child values overriding parent
- values.
-
- Args:
- parent_metadata: Metadata from parent folder
- child_metadata: Metadata from current folder
-
- Returns:
- Merged metadata dictionary
- """
- merged = parent_metadata.copy()
- merged.update(child_metadata)
- return merged
-
-
-def resolve_metadata_for_plot(
- plot_path: Path,
- inherited_metadata: Dict[str, Any]
-) -> Dict[str, Any]:
- """
- Resolve metadata for a specific plot by merging inherited metadata
- with plot-specific metadata.
-
- Args:
- plot_path: Path to the plot file (PDF)
- inherited_metadata: Metadata inherited from folder hierarchy
-
- Returns:
- Final merged metadata for the plot
- """
- plot_stem = plot_path.stem
- plot_dir = plot_path.parent
-
- # Check for plot-specific metadata files
- for suffix in ['.yaml', '.yml', '.json']:
- plot_metadata_path = plot_dir / f"{plot_stem}{suffix}"
- if plot_metadata_path.exists():
- plot_metadata = load_metadata_file(plot_metadata_path)
- return merge_metadata(inherited_metadata, plot_metadata)
-
- # No plot-specific metadata found, return inherited metadata
- return inherited_metadata.copy()
-
-
-def save_metadata_cache(
- web_dir: Path,
- plot_metadata_cache: Dict[str, Dict[str, Any]]
-) -> None:
- """
- Save plot metadata cache to meta_cache.json in the web directory.
-
- Args:
- web_dir: Web directory where the cache file should be saved
- plot_metadata_cache: Dictionary mapping plot names to their metadata
- """
- cache_path = web_dir / "meta_cache.json"
- try:
- with cache_path.open('w', encoding='utf-8') as f:
- json.dump(plot_metadata_cache, f, indent=2, ensure_ascii=False)
- logging.debug(f"Saved metadata cache: {cache_path}")
- except IOError as e:
- logging.warning(f"Could not save metadata cache {cache_path}: {e}")
diff --git a/orchestration/pdf_export.py b/orchestration/pdf_export.py
deleted file mode 100644
index 51df501..0000000
--- a/orchestration/pdf_export.py
+++ /dev/null
@@ -1,205 +0,0 @@
-"""
-PDF Export Module for Scientific Gallery Generator
-
-This module handles exporting and merging multiple plots into a single PDF
-using PyPDF2 and reportlab for layout.
-"""
-
-import io
-import tempfile
-from pathlib import Path
-from typing import List, Dict, Any
-import subprocess
-
-try:
- from PyPDF2 import PdfReader, PdfWriter
- from reportlab.pdfgen import canvas
- from reportlab.lib.pagesizes import letter, A4
- from reportlab.lib.utils import ImageReader
- HAS_PDF_LIBS = True
-except ImportError:
- HAS_PDF_LIBS = False
-
-
-class PDFExporter:
- """Handles exporting multiple plots to a merged PDF"""
-
- def __init__(self):
- self.page_size = A4
- self.margin = 50
-
- def merge_plots(self, plot_paths: List[str], layout: Dict[str, int],
- output_path: str = None) -> bytes:
- """
- Merge multiple PDF plots into a single PDF with grid layout.
-
- Args:
- plot_paths: List of paths to PDF files
- layout: Dictionary with 'rows' and 'cols' keys
- output_path: Optional output file path
-
- Returns:
- PDF bytes
- """
- if not HAS_PDF_LIBS:
- return self._merge_with_pdfjam(plot_paths, layout, output_path)
-
- return self._merge_with_pypdf(plot_paths, layout, output_path)
-
- def _merge_with_pypdf(self, plot_paths: List[str], layout: Dict[str, int],
- output_path: str = None) -> bytes:
- """Merge PDFs using PyPDF2 and reportlab"""
- from reportlab.pdfgen import canvas
- from reportlab.lib.pagesizes import A4
-
- # Create a new PDF with the layout
- buffer = io.BytesIO()
- c = canvas.Canvas(buffer, pagesize=A4)
- page_width, page_height = A4
-
- rows = layout['rows']
- cols = layout['cols']
-
- # Calculate dimensions for each plot
- plot_width = (page_width - 2 * self.margin) / cols
- plot_height = (page_height - 2 * self.margin) / rows
-
- # Place each plot in the grid
- for i, plot_path in enumerate(plot_paths):
- if i >= rows * cols:
- break
-
- row = i // cols
- col = i % cols
-
- # Calculate position
- x = self.margin + col * plot_width
- y = page_height - self.margin - (row + 1) * plot_height
-
- try:
- # Read the source PDF
- with open(plot_path, 'rb') as f:
- reader = PdfReader(f)
- if len(reader.pages) > 0:
- page = reader.pages[0]
-
- # Convert PDF page to image and place it
- # This is a simplified approach - in practice you'd want
- # to properly scale and position the PDF content
- self._draw_pdf_placeholder(c, x, y, plot_width, plot_height,
- Path(plot_path).stem)
-
- except Exception as e:
- print(f"Error processing {plot_path}: {e}")
- self._draw_error_placeholder(c, x, y, plot_width, plot_height)
-
- c.save()
-
- if output_path:
- with open(output_path, 'wb') as f:
- f.write(buffer.getvalue())
-
- return buffer.getvalue()
-
- def _merge_with_pdfjam(self, plot_paths: List[str], layout: Dict[str, int],
- output_path: str = None) -> bytes:
- """Merge PDFs using pdfjam (requires pdfpages LaTeX package)"""
- rows = layout['rows']
- cols = layout['cols']
-
- # Create temporary output file
- with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp_file:
- temp_output = tmp_file.name
-
- try:
- # Build pdfjam command
- cmd = [
- 'pdfjam',
- '--nup', f'{cols}x{rows}',
- '--landscape' if cols > rows else '--no-landscape',
- '--frame', 'true',
- '--delta', '10pt 10pt',
- '--offset', '0pt 0pt',
- '--outfile', temp_output
- ]
-
- # Add input files
- cmd.extend(plot_paths[:rows * cols])
-
- # Run pdfjam
- result = subprocess.run(cmd, capture_output=True, text=True)
-
- if result.returncode != 0:
- raise Exception(f"pdfjam failed: {result.stderr}")
-
- # Read the output file
- with open(temp_output, 'rb') as f:
- pdf_bytes = f.read()
-
- if output_path:
- with open(output_path, 'wb') as f:
- f.write(pdf_bytes)
-
- return pdf_bytes
-
- finally:
- # Clean up temporary file
- Path(temp_output).unlink(missing_ok=True)
-
- def _draw_pdf_placeholder(self, canvas, x: float, y: float, width: float,
- height: float, plot_name: str):
- """Draw a placeholder for a PDF plot"""
- # Draw border
- canvas.setStrokeColorRGB(0.5, 0.5, 0.5)
- canvas.setLineWidth(1)
- canvas.rect(x, y, width, height)
-
- # Draw plot name
- canvas.setFillColorRGB(0, 0, 0)
- canvas.setFont("Helvetica", 10)
- text_width = canvas.stringWidth(plot_name, "Helvetica", 10)
- text_x = x + (width - text_width) / 2
- text_y = y + height / 2
- canvas.drawString(text_x, text_y, plot_name)
-
- def _draw_error_placeholder(self, canvas, x: float, y: float, width: float,
- height: float):
- """Draw an error placeholder"""
- # Draw red border
- canvas.setStrokeColorRGB(1, 0, 0)
- canvas.setLineWidth(2)
- canvas.rect(x, y, width, height)
-
- # Draw error text
- canvas.setFillColorRGB(1, 0, 0)
- canvas.setFont("Helvetica-Bold", 12)
- error_text = "Error loading plot"
- text_width = canvas.stringWidth(error_text, "Helvetica-Bold", 12)
- text_x = x + (width - text_width) / 2
- text_y = y + height / 2
- canvas.drawString(text_x, text_y, error_text)
-
-
-def check_dependencies() -> Dict[str, bool]:
- """Check if required dependencies are available"""
- deps = {
- 'pypdf2': HAS_PDF_LIBS,
- 'pdfjam': False
- }
-
- # Check for pdfjam
- try:
- result = subprocess.run(['pdfjam', '--version'],
- capture_output=True, text=True)
- deps['pdfjam'] = result.returncode == 0
- except FileNotFoundError:
- pass
-
- return deps
-
-
-if __name__ == "__main__":
- # Test the exporter
- exporter = PDFExporter()
- deps = check_dependencies()
- print("Available dependencies:", deps)
diff --git a/python/add_creation_time.py b/python/add_creation_time.py
deleted file mode 100644
index 135d807..0000000
--- a/python/add_creation_time.py
+++ /dev/null
@@ -1,92 +0,0 @@
-#!/usr/bin/env python3
-"""
-Gallery Creation Time Integration
-Example function to add creation time metadata to plot items.
-This should be integrated into your existing Python gallery generation code.
-"""
-
-from pathlib import Path
-
-
-def add_creation_time_to_items(items, base_path):
- """
- Add creation time to plot items for sorting functionality.
-
- Args:
- items: List of plot item dictionaries
- base_path: Base path where plot files are located
-
- Returns:
- Updated items list with creation_time field
- """
- for item in items:
- try:
- # Try to get creation time from PNG file first, then PDF
- png_path = None
- pdf_path = None
-
- # Extract relative path from href
- if 'png_href' in item:
- png_rel_path = item['png_href'].replace('../', '').replace(
- './', '')
- png_path = Path(base_path) / png_rel_path
-
- if 'pdf_href' in item:
- pdf_rel_path = item['pdf_href'].replace('../', '').replace(
- './', '')
- pdf_path = Path(base_path) / pdf_rel_path
-
- # Use PNG creation time if available, otherwise PDF
- creation_time = 0
- if png_path and png_path.exists():
- creation_time = int(png_path.stat().st_ctime)
- elif pdf_path and pdf_path.exists():
- creation_time = int(pdf_path.stat().st_ctime)
-
- # Add creation time as timestamp (JavaScript can handle this)
- item['creation_time'] = creation_time
-
- except Exception as e:
- # Fallback to 0 if there's any error
- name = item.get('name', 'unknown')
- print(f"Warning: Could not get creation time for {name}: {e}")
- item['creation_time'] = 0
-
- return items
-
-
-def example_integration():
- """
- Example of how to integrate this into your existing gallery generation.
- """
- # This would be part of your existing gallery generation code
- items = [
- {
- 'name': 'plot1.png',
- 'png_href': './plot1.png',
- 'pdf_href': './plot1.pdf'
- },
- {
- 'name': 'plot2.png',
- 'png_href': './plot2.png',
- 'pdf_href': './plot2.pdf'
- }
- ]
-
- base_path = "/path/to/your/gallery/directory"
-
- # Add creation times
- items_with_time = add_creation_time_to_items(items, base_path)
-
- # Now items_with_time can be passed to your Jinja2 template
- # The template will have access to item.creation_time for each item
-
- return items_with_time
-
-
-if __name__ == "__main__":
- # Test the function
- items = example_integration()
- for item in items:
- created = item['creation_time']
- print(f"Plot: {item['name']}, Created: {created}")
diff --git a/python/add_metadata.py b/python/add_metadata.py
deleted file mode 100644
index cb2d5ef..0000000
--- a/python/add_metadata.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from typing import Dict, Any
-import json
-from pathlib import Path
-
-
-def open_metadata(path: str, filename: str = "metadata.json") -> Dict[str, Any]:
- """
- Open metadata file and return its contents as a dictionary.
-
- Args:
- path: The directory path where the metadata file is located.
- filename: The name of the metadata file (default: "metadata.json").
-
- Returns:
- A dictionary containing the metadata.
- """
- with open(Path(path) / filename, 'r', encoding='utf-8') as f:
- try:
- return json.load(f)
- except json.JSONDecodeError as e:
- raise ValueError(f"Error decoding JSON from {filename}: {e}")
- except FileNotFoundError:
- raise FileNotFoundError(f"Metadata file {filename} not found in {path}")
- except Exception as e:
- raise RuntimeError(f"Unexpected error reading metadata: {e}")
-
-
-def merge_metadata(
- base_metadata: Dict[str, Any],
- additional_metadata: Dict[str, Any]
-) -> Dict[str, Any]:
- """
- Merge two metadata dictionaries.
-
- Args:
- base_metadata: The base metadata dictionary.
- additional_metadata: The additional metadata dictionary to merge.
-
- Returns:
- A new dictionary containing the merged metadata.
- """
- merged = base_metadata.copy()
- for key, value in additional_metadata.items():
- merged[key] = value
- return merged
diff --git a/templates/gallery.html b/templates/gallery.html
deleted file mode 100644
index 35c7ea9..0000000
--- a/templates/gallery.html
+++ /dev/null
@@ -1,363 +0,0 @@
-
-
-
-
- {{ title }}
-
-
-
-
-
-
-
-
- {{ title }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% if folder_metadata %}
-
- {% endif %}
-
-
- {% if items %}
-
-
- Sort by:
-
- 📝 Name
-
-
- 🕒 Time
-
-
- ↑
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% endif %}
-
-
-
- {% for item in items %}
-
-
-
-
-
-
{{ item.name }}
-
- {% if item.creation_time and item.creation_time|int > 0 %}
- {{ item.creation_time|int|datetime_from_timestamp|strftime('%Y-%m-%d') }}
- {% else %}
- Unknown
- {% endif %}
-
-
-
- {% endfor %}
-
-
-
- {% if subdirs %}
- Subdirectories
-
- {% for sub in subdirs %}
- 📁 {{ sub }}
- {% endfor %}
-
- {% endif %}
-
-
-
-
-
-
-
-
-
- ⚖️
-
-
- ☀️
-
-
-
-
-
-
Keyboard Shortcuts
-
- Search
- Ctrl+K
-
-
- Recent plots
- Ctrl+R
-
-
- Compare plots
- Ctrl+C
-
-
- Export plots
- Ctrl+E
-
-
- Exit selection mode
- Esc
-
-
- Toggle theme
- Ctrl+T
-
-
- Toggle view
- Ctrl+V
-
-
- Sort by name
- Ctrl+N
-
-
- Sort by time
- Ctrl+M
-
-
- Toggle sort order
- Ctrl+O
-
-
- Help
- ?
-
-
-
-
-
-
- 📊 Files:
- 0
-
-
- 📁 Folders:
- 0
-
-
- 💾 Size:
- 0 KB
-
-
- 🕒 Updated:
- Now
-
-
-
-
-
-
-
-
-
-
-
-
-
- 📊 Click to select first plot
-
-
-
-
-
-
-
-
-
- 📊 Click to select second plot
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/test.ipynb b/test.ipynb
deleted file mode 100644
index 10fe056..0000000
--- a/test.ipynb
+++ /dev/null
@@ -1,235 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "code",
- "execution_count": 2,
- "id": "aedbb9f5",
- "metadata": {},
- "outputs": [],
- "source": [
- "from dataclasses import dataclass, field, asdict\n",
- "from pathlib import Path\n",
- "\n",
- "import yaml\n",
- "\n",
- "\n",
- "@dataclass\n",
- "class GalleryItem:\n",
- " name: str\n",
- " path: Path\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "id": "66624faf",
- "metadata": {},
- "outputs": [],
- "source": [
- "yaml_file = Path(\"config.yaml\")\n",
- "\n",
- "with open(yaml_file, \"r\") as f:\n",
- " new_config: dict = yaml.safe_load(f)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "id": "40a472f8",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "{'name': 'test_1_plot',\n",
- " 'path': 'data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1/plot__proc_3_5606769559__cat_incl__var_jet1_pt.pdf'}"
- ]
- },
- "execution_count": 8,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "new_config[\"sources\"][0]"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 10,
- "id": "745c97ad",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "[GalleryItem(name='test_1_plot', path=PosixPath('data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1/plot__proc_3_5606769559__cat_incl__var_jet1_pt.pdf')),\n",
- " GalleryItem(name='test_2_dir', path=PosixPath('data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1'))]"
- ]
- },
- "execution_count": 10,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "sources = [\n",
- " GalleryItem(name=src[\"name\"], path=Path(src[\"path\"]))\n",
- " for src in new_config.get(\"sources\", False)\n",
- "]\n",
- "sources"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 15,
- "id": "a0102f9d",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "Config(web_folder='', backup_folder='', png_dpi=400, plot_root='gallery', sources=[{'name': 'test_1_plot', 'path': 'data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1/plot__proc_3_5606769559__cat_incl__var_jet1_pt.pdf'}, {'name': 'test_2_dir', 'path': 'data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1/'}])"
- ]
- },
- "execution_count": 15,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "\n",
- "\n",
- "@dataclass\n",
- "class Config:\n",
- " web_folder: str = \"\"\n",
- " backup_folder: str = \"\"\n",
- " png_dpi: int = 400\n",
- " plot_root: str = \"gallery\"\n",
- " sources: list[GalleryItem] = field(default_factory=list)\n",
- "\n",
- " @classmethod\n",
- " def from_yaml(cls, yaml_file: str, strict: bool = False) -> \"Config\":\n",
- " \"\"\"\n",
- " Load configuration from a YAML file.\n",
- "\n",
- " Args:\n",
- " yaml_file (str): Path to the YAML file.\n",
- " strict (bool):\n",
- " If True, raises an error if a key in the YAML file does not exist in the Config class.\n",
- " If False (default), adds all keys as attributes\n",
- "\n",
- " Returns:\n",
- " Config: Instance of this class\n",
- " \"\"\"\n",
- "\n",
- " with open(yaml_file, \"r\") as f:\n",
- " new_config: dict = yaml.safe_load(f)\n",
- "\n",
- " new_config[\"sources\"] = [\n",
- " GalleryItem(name=src[\"name\"], path=Path(src[\"path\"]))\n",
- " for src in new_config.get(\"sources\", False)\n",
- " ]\n",
- "\n",
- " instance = cls(**{\n",
- " k: v\n",
- " for k, v in new_config.items()\n",
- " if hasattr(cls, k) or not strict\n",
- " })\n",
- "\n",
- " for key in new_config.keys():\n",
- " if strict and not hasattr(instance, key):\n",
- " raise KeyError(f\"Key '{key}' not found in Config class\")\n",
- "\n",
- " return instance\n",
- "\n",
- " def to_yaml(self, yaml_file: str) -> None:\n",
- " \"\"\"\n",
- " Save the current configuration to a YAML file.\n",
- "\n",
- " Args:\n",
- " yaml_file (str): Path to the YAML file.\n",
- " \"\"\"\n",
- " with open(yaml_file, \"w\") as f:\n",
- " yaml.dump(asdict(self), f, default_flow_style=False)\n",
- "\n",
- "\n",
- "instance = Config(**{\n",
- " k: v\n",
- " for k, v in new_config.items()\n",
- " #if hasattr(Config, k)\n",
- "})\n",
- "instance"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 17,
- "id": "0b516ae9",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "[{'name': 'test_1_plot',\n",
- " 'path': 'data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1/plot__proc_3_5606769559__cat_incl__var_jet1_pt.pdf'},\n",
- " {'name': 'test_2_dir',\n",
- " 'path': 'data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1/'}]"
- ]
- },
- "execution_count": 17,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "new_config[\"sources\"]"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 18,
- "id": "b047f0c5",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "[{'name': 'test_1_plot',\n",
- " 'path': 'data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1/plot__proc_3_5606769559__cat_incl__var_jet1_pt.pdf'},\n",
- " {'name': 'test_2_dir',\n",
- " 'path': 'data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1/'}]"
- ]
- },
- "execution_count": 18,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "instance.sources"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.12.1"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 5
-}
diff --git a/test_logger.py b/test_logger.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/test_backup.py b/tests/test_backup.py
deleted file mode 100644
index 9890cce..0000000
--- a/tests/test_backup.py
+++ /dev/null
@@ -1,137 +0,0 @@
-import datetime
-import zipfile
-from unittest.mock import patch
-
-from gallery.utils.backup import create_backup
-
-
-def test_backup_creates_zip(tmp_path):
- web_folder = tmp_path / "plots"
- web_folder.mkdir()
- (web_folder / "file1.txt").write_text("abc")
- (web_folder / "file2.txt").write_text("def")
- backup_folder = tmp_path / "backups"
- backup_folder.mkdir()
-
- assert create_backup(web_folder, backup_folder) is True
-
- today = datetime.date.today().strftime("%Y%m%d")
- backup_path = backup_folder / f"backup-{today}.zip"
-
- assert backup_path.exists()
- with zipfile.ZipFile(backup_path, "r") as z:
- names = z.namelist()
- assert any("file1.txt" in n for n in names)
- assert any("file2.txt" in n for n in names)
-
-
-def test_backup_with_subdirectories(tmp_path):
- web_folder = tmp_path / "plots"
- web_folder.mkdir()
- (web_folder / "file1.txt").write_text("content1")
-
- subdir = web_folder / "subdir"
- subdir.mkdir()
- (subdir / "file2.txt").write_text("content2")
-
- nested_subdir = subdir / "nested"
- nested_subdir.mkdir()
- (nested_subdir / "file3.txt").write_text("content3")
-
- backup_folder = tmp_path / "backups"
- backup_folder.mkdir()
-
- assert create_backup(web_folder, backup_folder) is True
-
- today = datetime.date.today().strftime("%Y%m%d")
- backup_path = backup_folder / f"backup-{today}.zip"
-
- assert backup_path.exists()
- with zipfile.ZipFile(backup_path, "r") as z:
- names = z.namelist()
- assert any("file1.txt" in n for n in names)
- assert any("file2.txt" in n for n in names)
- assert any("file3.txt" in n for n in names)
-
-
-def test_backup_existing_file_is_not_overwritten(tmp_path):
- web_folder = tmp_path / "plots"
- web_folder.mkdir()
- (web_folder / "file1.txt").write_text("abc")
-
- backup_folder = tmp_path / "backups"
- backup_folder.mkdir()
-
- today = datetime.date.today().strftime("%Y%m%d")
- backup_path = backup_folder / f"backup-{today}.zip"
- backup_path.write_text("existing backup")
-
- assert create_backup(web_folder, backup_folder) is True
- assert backup_path.read_text() == "existing backup"
-
-
-def test_backup_empty_folder(tmp_path):
- web_folder = tmp_path / "plots"
- web_folder.mkdir()
-
- backup_folder = tmp_path / "backups"
- backup_folder.mkdir()
-
- assert create_backup(web_folder, backup_folder) is True
-
- today = datetime.date.today().strftime("%Y%m%d")
- backup_path = backup_folder / f"backup-{today}.zip"
-
- assert backup_path.exists()
- with zipfile.ZipFile(backup_path, "r") as z:
- assert len(z.namelist()) == 0
-
-
-def test_backup_nonexistent_web_folder(tmp_path):
- web_folder = tmp_path / "nonexistent_plots"
- backup_folder = tmp_path / "backups"
- backup_folder.mkdir()
-
- assert create_backup(web_folder, backup_folder) is True
-
- today = datetime.date.today().strftime("%Y%m%d")
- backup_path = backup_folder / f"backup-{today}.zip"
-
- assert backup_path.exists()
- with zipfile.ZipFile(backup_path, "r") as z:
- assert len(z.namelist()) == 0
-
-
-@patch("datetime.date")
-def test_backup_with_custom_date(mock_date, tmp_path):
- mock_date.today.return_value.strftime.return_value = "20230908"
-
- web_folder = tmp_path / "plots"
- web_folder.mkdir()
- (web_folder / "file1.txt").write_text("test")
-
- backup_folder = tmp_path / "backups"
- backup_folder.mkdir()
-
- assert create_backup(web_folder, backup_folder) is True
-
- backup_path = backup_folder / "backup-20230908.zip"
- assert backup_path.exists()
-
-
-def test_backup_folder_creation(tmp_path):
- web_folder = tmp_path / "plots"
- web_folder.mkdir()
- (web_folder / "file1.txt").write_text("test")
-
- # Don't create backup folder - let create_backup() create it
- backup_folder = tmp_path / "new_backups"
-
- assert create_backup(web_folder, backup_folder) is True
-
- assert backup_folder.exists()
- assert backup_folder.is_dir()
-
- today = datetime.date.today().strftime("%Y%m%d")
- backup_path = backup_folder / f"backup-{today}.zip"
- assert backup_path.exists()
diff --git a/tests/test_config.py b/tests/test_config.py
index c776aa0..71b33b9 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -28,7 +28,6 @@ def test_gallery_config_defaults():
assert cfg.plot_root == GalleryDefaults.plot_root
assert cfg.cache_enabled == GalleryDefaults.cache_enabled
assert cfg.inherit_from_parent == GalleryDefaults.inherit_from_parent
- assert cfg.backup_folder == ""
def test_gallery_config_sources_from_dicts():
@@ -47,7 +46,7 @@ def test_gallery_config_sources_invalid_type():
def test_gallery_config_from_yaml(tmp_path):
yaml_content = {
"paths": {"web_folder": "/test/web"},
- "gallery": {"plot_root": "test_plots", "png_dpi": 200, "backup_folder": "test_backups"},
+ "gallery": {"plot_root": "test_plots", "png_dpi": 200},
"metadata": {"cache_enabled": False, "inherit_from_parent": False},
"sources": [
{"name": "source1", "path": "/path1"},
@@ -64,7 +63,6 @@ def test_gallery_config_from_yaml(tmp_path):
assert cfg.web_folder == Path("/test/web")
assert cfg.plot_root == "test_plots"
assert cfg.png_dpi == 200
- assert cfg.backup_folder == "test_backups"
assert cfg.cache_enabled is False
assert cfg.inherit_from_parent is False
assert len(cfg.sources) == 2
@@ -106,7 +104,6 @@ def test_gallery_config_to_yaml_round_trip(tmp_path):
sources=[{"name": "test", "path": "/test"}],
plot_root="plots",
png_dpi=300,
- backup_folder="backups",
)
yaml_file = tmp_path / "output_config.yaml"
@@ -118,7 +115,6 @@ def test_gallery_config_to_yaml_round_trip(tmp_path):
assert reloaded.web_folder == cfg.web_folder
assert reloaded.plot_root == cfg.plot_root
assert reloaded.png_dpi == cfg.png_dpi
- assert reloaded.backup_folder == cfg.backup_folder
assert reloaded.sources[0].name == "test"