Remove pre-package-restructure cruft and dead features
Delete the top-level implementation superseded by the gallery/ package conversion (generate_gallery.py, orchestration/, root templates/ and assets/, python/ scripts), stray scratch files, and docs describing a container/GitLab-CI coverage workflow that no longer exists. Also drop two half-wired, never-invoked features: the backup_folder config/TUI option (create_backup() was never called from the pipeline) and the unfinished export-to-LaTeX JS/CSS. Update README's install instructions to the current Gitea remote.
This commit is contained in:
Vendored
-6
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"flake8.args": [
|
|
||||||
"--max-line-length=120",
|
|
||||||
"--ignore=W293,E123,W503",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -38,12 +38,12 @@
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### From GitLab
|
### From Gitea
|
||||||
|
|
||||||
Pip install:
|
Pip install:
|
||||||
|
|
||||||
```bash
|
```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:
|
Or git clone and `pip install .`. After installation the `gallery` command is available in your shell. Verify with:
|
||||||
@@ -184,7 +184,6 @@ paths:
|
|||||||
gallery:
|
gallery:
|
||||||
plot_root: "gallery" # subdirectory inside web_folder
|
plot_root: "gallery" # subdirectory inside web_folder
|
||||||
png_dpi: 400 # thumbnail resolution
|
png_dpi: 400 # thumbnail resolution
|
||||||
backup_folder: "" # optional backup path
|
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
- name: "analysis_results"
|
- name: "analysis_results"
|
||||||
|
|||||||
Binary file not shown.
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
/**
|
|
||||||
* Plot comparison functionality
|
|
||||||
*/
|
|
||||||
export class ComparisonManager {
|
|
||||||
constructor() {
|
|
||||||
this.comparisonMode = false;
|
|
||||||
this.comparisonSlot = null; // 'left' or 'right'
|
|
||||||
this.plots = { left: null, right: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle comparison mode
|
|
||||||
*/
|
|
||||||
toggleCompareMode() {
|
|
||||||
this.comparisonMode = !this.comparisonMode;
|
|
||||||
const compareBtn = document.getElementById('compareToggle');
|
|
||||||
|
|
||||||
if (!compareBtn) return;
|
|
||||||
|
|
||||||
if (this.comparisonMode) {
|
|
||||||
compareBtn.style.background = 'var(--success-color)';
|
|
||||||
compareBtn.title = 'Exit Compare Mode (Ctrl+C)';
|
|
||||||
this.showComparisonOverlay();
|
|
||||||
} else {
|
|
||||||
compareBtn.style.background = 'var(--button-bg)';
|
|
||||||
compareBtn.title = 'Compare Plots (Ctrl+C)';
|
|
||||||
this.hideComparisonOverlay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show comparison overlay
|
|
||||||
*/
|
|
||||||
showComparisonOverlay() {
|
|
||||||
const overlay = document.getElementById('comparisonOverlay');
|
|
||||||
if (overlay) overlay.classList.add('open');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide comparison overlay
|
|
||||||
*/
|
|
||||||
hideComparisonOverlay() {
|
|
||||||
const overlay = document.getElementById('comparisonOverlay');
|
|
||||||
if (overlay) overlay.classList.remove('open');
|
|
||||||
this.comparisonMode = false;
|
|
||||||
|
|
||||||
const compareBtn = document.getElementById('compareToggle');
|
|
||||||
if (compareBtn) {
|
|
||||||
compareBtn.style.background = 'var(--button-bg)';
|
|
||||||
compareBtn.title = 'Compare Plots (Ctrl+C)';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close comparison overlay (alias for hideComparisonOverlay)
|
|
||||||
*/
|
|
||||||
closeComparison() {
|
|
||||||
this.hideComparisonOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select plot for comparison - simple approach
|
|
||||||
*/
|
|
||||||
selectPlotForComparison(slot) {
|
|
||||||
this.comparisonSlot = slot;
|
|
||||||
|
|
||||||
// Hide overlay temporarily by removing the 'open' class
|
|
||||||
const overlay = document.getElementById('comparisonOverlay');
|
|
||||||
if (overlay) overlay.classList.remove('open');
|
|
||||||
|
|
||||||
// Show simple alert with instructions
|
|
||||||
const instruction = document.createElement('div');
|
|
||||||
instruction.id = 'comparisonInstruction';
|
|
||||||
instruction.style.cssText = `
|
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
background: var(--button-bg);
|
|
||||||
color: white;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
z-index: 3000;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
|
||||||
`;
|
|
||||||
instruction.innerHTML = `📊 Click any plot to select for ${slot === 'left' ? 'Plot A' : 'Plot B'} (ESC to cancel)`;
|
|
||||||
document.body.appendChild(instruction);
|
|
||||||
|
|
||||||
// Add one-time click listener to all grid items
|
|
||||||
const gridItems = document.querySelectorAll('.grid-item');
|
|
||||||
const handleClick = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
const gridItem = event.currentTarget;
|
|
||||||
const img = gridItem.querySelector('img');
|
|
||||||
const nameEl = gridItem.querySelector('.plot-name');
|
|
||||||
|
|
||||||
if (img && nameEl) {
|
|
||||||
const plotInfo = {
|
|
||||||
name: nameEl.textContent.trim(),
|
|
||||||
imgSrc: img.src,
|
|
||||||
pdfSrc: img.src.replace('.png', '.pdf'),
|
|
||||||
path: window.location.pathname
|
|
||||||
};
|
|
||||||
|
|
||||||
this.addPlotToComparison(plotInfo, slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
instruction.remove();
|
|
||||||
gridItems.forEach(item => item.removeEventListener('click', handleClick));
|
|
||||||
this.comparisonSlot = null;
|
|
||||||
if (overlay) overlay.classList.add('open');
|
|
||||||
};
|
|
||||||
|
|
||||||
gridItems.forEach(item => {
|
|
||||||
item.style.cursor = 'pointer';
|
|
||||||
item.style.border = '2px dashed var(--button-bg)';
|
|
||||||
item.addEventListener('click', handleClick);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ESC to cancel
|
|
||||||
const handleEscape = (event) => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
instruction.remove();
|
|
||||||
gridItems.forEach(item => {
|
|
||||||
item.removeEventListener('click', handleClick);
|
|
||||||
item.style.cursor = '';
|
|
||||||
item.style.border = '';
|
|
||||||
});
|
|
||||||
this.comparisonSlot = null;
|
|
||||||
if (overlay) overlay.classList.add('open');
|
|
||||||
document.removeEventListener('keydown', handleEscape);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('keydown', handleEscape);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add plot to comparison panel
|
|
||||||
*/
|
|
||||||
addPlotToComparison(plotInfo, slot) {
|
|
||||||
this.plots[slot] = plotInfo;
|
|
||||||
|
|
||||||
const container = document.getElementById(`${slot}PlotContainer`);
|
|
||||||
const title = document.getElementById(`${slot}PlotTitle`);
|
|
||||||
const replaceBtn = document.getElementById(`${slot}ReplaceBtn`);
|
|
||||||
|
|
||||||
if (container) {
|
|
||||||
container.innerHTML = `
|
|
||||||
<img src="${plotInfo.imgSrc}" class="comparison-plot" alt="${plotInfo.name}"
|
|
||||||
onclick="window.open('${plotInfo.pdfSrc}', '_blank')" />
|
|
||||||
<div class="comparison-plot-info">
|
|
||||||
<strong>${plotInfo.name}</strong><br>
|
|
||||||
<small>${plotInfo.path}</small>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (title) title.textContent = plotInfo.name;
|
|
||||||
if (replaceBtn) replaceBtn.style.display = 'block';
|
|
||||||
|
|
||||||
// Reset grid item styles
|
|
||||||
const gridItems = document.querySelectorAll('.grid-item');
|
|
||||||
gridItems.forEach(item => {
|
|
||||||
item.style.cursor = '';
|
|
||||||
item.style.border = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace plot in comparison
|
|
||||||
*/
|
|
||||||
replacePlot(slot) {
|
|
||||||
this.selectPlotForComparison(slot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,448 +0,0 @@
|
|||||||
/**
|
|
||||||
* Export Manager for Gallery
|
|
||||||
* Handles exporting selected plots to merged PDF
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class ExportManager {
|
|
||||||
constructor() {
|
|
||||||
this.selectedPlots = new Set();
|
|
||||||
this.maxPlots = 4;
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.createExportButton();
|
|
||||||
this.bindEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the export button in the floating buttons section
|
|
||||||
*/
|
|
||||||
createExportButton() {
|
|
||||||
const floatingButtons = document.querySelector('.floating-buttons');
|
|
||||||
if (!floatingButtons) return;
|
|
||||||
|
|
||||||
const exportBtn = document.createElement('button');
|
|
||||||
exportBtn.className = 'floating-btn export-btn';
|
|
||||||
exportBtn.id = 'exportBtn';
|
|
||||||
exportBtn.title = 'Export Selected Plots (Ctrl+E)';
|
|
||||||
exportBtn.innerHTML = '📄';
|
|
||||||
exportBtn.style.display = 'none'; // Hidden by default
|
|
||||||
exportBtn.onclick = () => this.exportSelectedPlots();
|
|
||||||
|
|
||||||
floatingButtons.appendChild(exportBtn);
|
|
||||||
|
|
||||||
// Add selection counter
|
|
||||||
const selectionCounter = document.createElement('div');
|
|
||||||
selectionCounter.className = 'selection-counter';
|
|
||||||
selectionCounter.id = 'selectionCounter';
|
|
||||||
selectionCounter.style.display = 'none';
|
|
||||||
selectionCounter.innerHTML = '0/4 selected';
|
|
||||||
floatingButtons.appendChild(selectionCounter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bind events for plot selection
|
|
||||||
*/
|
|
||||||
bindEvents() {
|
|
||||||
// Add selection mode toggle
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.ctrlKey && e.key === 'e') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.toggleSelectionMode();
|
|
||||||
}
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
this.exitSelectionMode();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add selection handlers to existing plots
|
|
||||||
this.addSelectionHandlers();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add selection handlers to all plot items
|
|
||||||
*/
|
|
||||||
addSelectionHandlers() {
|
|
||||||
const plotItems = document.querySelectorAll('.grid-item');
|
|
||||||
plotItems.forEach(item => this.addSelectionHandler(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add selection handler to a single plot item
|
|
||||||
*/
|
|
||||||
addSelectionHandler(item) {
|
|
||||||
// Create selection overlay
|
|
||||||
const overlay = document.createElement('div');
|
|
||||||
overlay.className = 'selection-overlay';
|
|
||||||
overlay.innerHTML = `
|
|
||||||
<div class="selection-checkbox">
|
|
||||||
<span class="checkbox-icon">☐</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
item.appendChild(overlay);
|
|
||||||
|
|
||||||
// Add click handler for selection
|
|
||||||
overlay.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.togglePlotSelection(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle selection mode
|
|
||||||
*/
|
|
||||||
toggleSelectionMode() {
|
|
||||||
const body = document.body;
|
|
||||||
const isSelectionMode = body.classList.contains('selection-mode');
|
|
||||||
|
|
||||||
if (isSelectionMode) {
|
|
||||||
this.exitSelectionMode();
|
|
||||||
} else {
|
|
||||||
this.enterSelectionMode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enter selection mode
|
|
||||||
*/
|
|
||||||
enterSelectionMode() {
|
|
||||||
document.body.classList.add('selection-mode');
|
|
||||||
document.getElementById('exportBtn').style.display = 'block';
|
|
||||||
document.getElementById('selectionCounter').style.display = 'block';
|
|
||||||
this.updateSelectionCounter();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exit selection mode
|
|
||||||
*/
|
|
||||||
exitSelectionMode() {
|
|
||||||
const wasInSelectionMode = document.body.classList.contains('selection-mode');
|
|
||||||
|
|
||||||
document.body.classList.remove('selection-mode');
|
|
||||||
document.getElementById('exportBtn').style.display = 'none';
|
|
||||||
document.getElementById('selectionCounter').style.display = 'none';
|
|
||||||
this.clearSelection();
|
|
||||||
|
|
||||||
// Show message if user was actually in selection mode
|
|
||||||
if (wasInSelectionMode) {
|
|
||||||
this.showMessage('Exited selection mode', 'info');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle plot selection
|
|
||||||
*/
|
|
||||||
togglePlotSelection(item) {
|
|
||||||
const plotName = this.getPlotName(item);
|
|
||||||
const plotPath = this.getPlotPath(item);
|
|
||||||
|
|
||||||
if (this.selectedPlots.has(plotName)) {
|
|
||||||
this.selectedPlots.delete(plotName);
|
|
||||||
item.classList.remove('selected');
|
|
||||||
item.querySelector('.checkbox-icon').textContent = '☐';
|
|
||||||
} else {
|
|
||||||
if (this.selectedPlots.size >= this.maxPlots) {
|
|
||||||
this.showMessage(`Maximum ${this.maxPlots} plots can be selected`, 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.selectedPlots.add(plotName);
|
|
||||||
item.classList.add('selected');
|
|
||||||
item.querySelector('.checkbox-icon').textContent = '☑';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateSelectionCounter();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get plot name from grid item
|
|
||||||
*/
|
|
||||||
getPlotName(item) {
|
|
||||||
const plotName = item.querySelector('.plot-name');
|
|
||||||
return plotName ? plotName.textContent.trim() : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get plot PDF path from grid item
|
|
||||||
*/
|
|
||||||
getPlotPath(item) {
|
|
||||||
const link = item.querySelector('a[href$=".pdf"]');
|
|
||||||
return link ? link.href : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update selection counter
|
|
||||||
*/
|
|
||||||
updateSelectionCounter() {
|
|
||||||
const counter = document.getElementById('selectionCounter');
|
|
||||||
if (counter) {
|
|
||||||
counter.textContent = `${this.selectedPlots.size}/${this.maxPlots} selected`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportBtn = document.getElementById('exportBtn');
|
|
||||||
if (exportBtn) {
|
|
||||||
exportBtn.disabled = this.selectedPlots.size === 0;
|
|
||||||
exportBtn.style.opacity = this.selectedPlots.size === 0 ? '0.5' : '1';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all selections
|
|
||||||
*/
|
|
||||||
clearSelection() {
|
|
||||||
this.selectedPlots.clear();
|
|
||||||
document.querySelectorAll('.grid-item.selected').forEach(item => {
|
|
||||||
item.classList.remove('selected');
|
|
||||||
const checkbox = item.querySelector('.checkbox-icon');
|
|
||||||
if (checkbox) checkbox.textContent = '☐';
|
|
||||||
});
|
|
||||||
this.updateSelectionCounter();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export selected plots to merged PDF
|
|
||||||
*/
|
|
||||||
async exportSelectedPlots() {
|
|
||||||
if (this.selectedPlots.size === 0) {
|
|
||||||
this.showMessage('No plots selected', 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const plotPaths = Array.from(this.selectedPlots).map(plotName => {
|
|
||||||
const item = Array.from(document.querySelectorAll('.grid-item'))
|
|
||||||
.find(item => this.getPlotName(item) === plotName);
|
|
||||||
return this.getPlotPath(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.showMessage('Preparing export...', 'info');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.createMergedPDF(plotPaths);
|
|
||||||
} catch (error) {
|
|
||||||
this.showMessage('Export failed: ' + error.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create merged PDF using Python script
|
|
||||||
*/
|
|
||||||
async createMergedPDF(plotPaths) {
|
|
||||||
// Convert file:// URLs to actual paths
|
|
||||||
const actualPaths = plotPaths.map(url => {
|
|
||||||
if (url.startsWith('file://')) {
|
|
||||||
return url.substring(7); // Remove 'file://' prefix
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
});
|
|
||||||
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
|
|
||||||
const outputName = `merged_plots_${timestamp}.pdf`;
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
plots: actualPaths,
|
|
||||||
layout: this.calculateLayout(actualPaths.length),
|
|
||||||
output_name: outputName
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate a unique temporary filename
|
|
||||||
const tempFileName = `export_request_${Date.now()}.json`;
|
|
||||||
|
|
||||||
// Get work directory from config or fallback
|
|
||||||
const workDir = window.galleryConfig?.workDir || '/work/kschmidt/web';
|
|
||||||
|
|
||||||
// Save the request to a JSON file that can be picked up by a Python script
|
|
||||||
const requestData = JSON.stringify(payload, null, 2);
|
|
||||||
|
|
||||||
// Show improved export instructions with full command
|
|
||||||
this.showExportInstructions(requestData, tempFileName, workDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate optimal layout for given number of plots
|
|
||||||
*/
|
|
||||||
calculateLayout(numPlots) {
|
|
||||||
switch (numPlots) {
|
|
||||||
case 1: return { rows: 1, cols: 1 };
|
|
||||||
case 2: return { rows: 1, cols: 2 };
|
|
||||||
case 3: return { rows: 2, cols: 2 }; // 3 plots in 2x2 grid with one empty
|
|
||||||
case 4: return { rows: 2, cols: 2 };
|
|
||||||
default: return { rows: 2, cols: 2 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show export instructions to user
|
|
||||||
*/
|
|
||||||
showExportInstructions(requestData, tempFileName, workDir) {
|
|
||||||
const tempFilePath = `/tmp/${tempFileName}`;
|
|
||||||
const fullCommand = `echo '${requestData.replace(/'/g, "'\\''")}' > ${tempFilePath} && cd ${workDir} && python export_plots.py ${tempFilePath}`;
|
|
||||||
|
|
||||||
const instructions = `
|
|
||||||
<div class="export-instructions">
|
|
||||||
<h3>🚀 Export Selected Plots</h3>
|
|
||||||
<p>Run the following command in your terminal to export the selected plots:</p>
|
|
||||||
|
|
||||||
<div class="export-command-container">
|
|
||||||
<div class="export-command">
|
|
||||||
<code id="exportCommand">${fullCommand}</code>
|
|
||||||
</div>
|
|
||||||
<div class="export-actions">
|
|
||||||
<button onclick="this.copyCommand()" class="copy-btn" title="Copy command to clipboard">
|
|
||||||
📋 Copy Command
|
|
||||||
</button>
|
|
||||||
<button onclick="this.copyJSON()" class="copy-btn" title="Copy JSON only">
|
|
||||||
📄 Copy JSON
|
|
||||||
</button>
|
|
||||||
<button onclick="this.close()" class="close-btn">
|
|
||||||
✕ Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="export-details">
|
|
||||||
<h4>📋 Command Breakdown:</h4>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Creates temporary file:</strong> <code>${tempFilePath}</code></li>
|
|
||||||
<li><strong>Changes to work directory:</strong> <code>${workDir}</code></li>
|
|
||||||
<li><strong>Runs export script:</strong> <code>python export_plots.py</code></li>
|
|
||||||
<li><strong>Output file:</strong> Will be saved in the work directory</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="export-tips">
|
|
||||||
<h4>💡 Tips:</h4>
|
|
||||||
<ul>
|
|
||||||
<li>The temporary JSON file will be automatically cleaned up after successful export</li>
|
|
||||||
<li>Use <kbd>Esc</kbd> to exit selection mode</li>
|
|
||||||
<li>Press <kbd>Ctrl+E</kbd> to toggle selection mode</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const overlay = document.createElement('div');
|
|
||||||
overlay.className = 'export-overlay';
|
|
||||||
overlay.innerHTML = instructions;
|
|
||||||
|
|
||||||
// Add methods to the overlay for button handlers
|
|
||||||
overlay.copyCommand = function() {
|
|
||||||
navigator.clipboard.writeText(fullCommand).then(() => {
|
|
||||||
this.showCopyFeedback('Command copied to clipboard!');
|
|
||||||
}).catch(() => {
|
|
||||||
this.showCopyFeedback('Failed to copy. Please select and copy manually.', 'error');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
overlay.copyJSON = function() {
|
|
||||||
navigator.clipboard.writeText(requestData).then(() => {
|
|
||||||
this.showCopyFeedback('JSON copied to clipboard!');
|
|
||||||
}).catch(() => {
|
|
||||||
this.showCopyFeedback('Failed to copy. Please select and copy manually.', 'error');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
overlay.close = function() {
|
|
||||||
this.remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
overlay.showCopyFeedback = function(message, type = 'success') {
|
|
||||||
const feedback = document.createElement('div');
|
|
||||||
feedback.className = `copy-feedback copy-feedback-${type}`;
|
|
||||||
feedback.textContent = message;
|
|
||||||
this.appendChild(feedback);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (feedback.parentNode) {
|
|
||||||
feedback.parentNode.removeChild(feedback);
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
|
|
||||||
// Close on ESC key
|
|
||||||
const handleEscape = (e) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
overlay.remove();
|
|
||||||
document.removeEventListener('keydown', handleEscape);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('keydown', handleEscape);
|
|
||||||
|
|
||||||
// Close on clicking outside
|
|
||||||
overlay.addEventListener('click', (e) => {
|
|
||||||
if (e.target === overlay) {
|
|
||||||
overlay.remove();
|
|
||||||
document.removeEventListener('keydown', handleEscape);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download the PDF blob
|
|
||||||
*/
|
|
||||||
downloadPDF(blob) {
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = `merged_plots_${new Date().toISOString().split('T')[0]}.pdf`;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show temporary message to user
|
|
||||||
*/
|
|
||||||
showMessage(text, type = 'info') {
|
|
||||||
// Remove existing message
|
|
||||||
const existing = document.querySelector('.export-message');
|
|
||||||
if (existing) existing.remove();
|
|
||||||
|
|
||||||
const message = document.createElement('div');
|
|
||||||
message.className = `export-message export-message-${type}`;
|
|
||||||
message.textContent = text;
|
|
||||||
|
|
||||||
document.body.appendChild(message);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (message.parentNode) {
|
|
||||||
message.parentNode.removeChild(message);
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add these methods to ExportManager if not present
|
|
||||||
ExportManager.prototype.isSelectionModeActive = function() {
|
|
||||||
return document.body.classList.contains('selection-mode');
|
|
||||||
};
|
|
||||||
ExportManager.prototype.exitSelectionMode = function() {
|
|
||||||
document.body.classList.remove('selection-mode');
|
|
||||||
if (typeof this.clearSelection === 'function') {
|
|
||||||
this.clearSelection();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ensure a single global instance
|
|
||||||
window.exportManager = window.exportManager || new ExportManager();
|
|
||||||
|
|
||||||
// Listen for ESC key globally to exit selection mode
|
|
||||||
// (This will work even if focus is not on a plot)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape' && window.exportManager && window.exportManager.isSelectionModeActive()) {
|
|
||||||
window.exportManager.exitSelectionMode();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Attach improved export logic to export button
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const exportBtn = document.getElementById('exportBtn');
|
|
||||||
if (exportBtn) {
|
|
||||||
exportBtn.addEventListener('click', function() {
|
|
||||||
window.exportManager.exportSelectedPlots();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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); }
|
|
||||||
}
|
|
||||||
@@ -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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
});
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
/**
|
|
||||||
* Metadata Popup functionality for Gallery
|
|
||||||
*
|
|
||||||
* Handles showing metadata in small popups overlaid on plot thumbnails
|
|
||||||
*/
|
|
||||||
|
|
||||||
class MetadataPopup {
|
|
||||||
constructor() {
|
|
||||||
this.activePopup = null;
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// Close popup when clicking outside
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
if (!e.target.closest('.metadata-btn') && !e.target.closest('.metadata-popup')) {
|
|
||||||
this.hidePopup();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close popup on Escape key
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
this.hidePopup();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
showPopup(button, plotName, metadata) {
|
|
||||||
// Hide any existing popup
|
|
||||||
this.hidePopup();
|
|
||||||
|
|
||||||
// Create popup element
|
|
||||||
const popup = document.createElement('div');
|
|
||||||
popup.className = 'metadata-popup';
|
|
||||||
popup.innerHTML = this.formatMetadata(plotName, metadata);
|
|
||||||
|
|
||||||
// Position popup relative to button
|
|
||||||
const rect = button.getBoundingClientRect();
|
|
||||||
popup.style.position = 'fixed';
|
|
||||||
popup.style.left = rect.left + 'px';
|
|
||||||
popup.style.top = (rect.bottom + 5) + 'px';
|
|
||||||
popup.style.zIndex = '1000';
|
|
||||||
|
|
||||||
// Add to DOM
|
|
||||||
document.body.appendChild(popup);
|
|
||||||
this.activePopup = popup;
|
|
||||||
|
|
||||||
// Adjust position if popup goes off screen
|
|
||||||
setTimeout(() => {
|
|
||||||
const popupRect = popup.getBoundingClientRect();
|
|
||||||
|
|
||||||
// Adjust horizontal position
|
|
||||||
if (popupRect.right > window.innerWidth) {
|
|
||||||
popup.style.left = (rect.right - popupRect.width) + 'px';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust vertical position
|
|
||||||
if (popupRect.bottom > window.innerHeight) {
|
|
||||||
popup.style.top = (rect.top - popupRect.height - 5) + 'px';
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// Animate in
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
popup.classList.add('show');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
hidePopup() {
|
|
||||||
if (this.activePopup) {
|
|
||||||
this.activePopup.classList.remove('show');
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.activePopup && this.activePopup.parentNode) {
|
|
||||||
this.activePopup.parentNode.removeChild(this.activePopup);
|
|
||||||
}
|
|
||||||
this.activePopup = null;
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formatMetadata(plotName, metadata) {
|
|
||||||
if (!metadata || Object.keys(metadata).length === 0) {
|
|
||||||
return `
|
|
||||||
<div class="metadata-popup-header">
|
|
||||||
<h4>${plotName}</h4>
|
|
||||||
</div>
|
|
||||||
<div class="metadata-popup-content">
|
|
||||||
<p class="no-metadata">No metadata available</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = `
|
|
||||||
<div class="metadata-popup-header">
|
|
||||||
<h4>${plotName}</h4>
|
|
||||||
</div>
|
|
||||||
<div class="metadata-popup-content">
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Show priority fields first
|
|
||||||
const priorityFields = ['title', 'description', 'plot_type', 'experiment'];
|
|
||||||
const processedKeys = new Set();
|
|
||||||
|
|
||||||
// Display priority fields first
|
|
||||||
for (const key of priorityFields) {
|
|
||||||
if (metadata[key] !== undefined) {
|
|
||||||
html += this.formatMetadataField(key, metadata[key]);
|
|
||||||
processedKeys.add(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show file info if available
|
|
||||||
if (metadata.file_info) {
|
|
||||||
html += `<div class="metadata-section-title">File Information</div>`;
|
|
||||||
html += this.formatMetadataField('File Size', metadata.file_info.size);
|
|
||||||
if (metadata.file_info.extension) {
|
|
||||||
html += this.formatMetadataField('Format', metadata.file_info.extension);
|
|
||||||
}
|
|
||||||
processedKeys.add('file_info');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show timestamps if available
|
|
||||||
if (metadata.timestamps) {
|
|
||||||
html += `<div class="metadata-section-title">Timestamps</div>`;
|
|
||||||
if (metadata.timestamps.created_human) {
|
|
||||||
html += this.formatMetadataField('Created', metadata.timestamps.created_human);
|
|
||||||
}
|
|
||||||
if (metadata.timestamps.modified_human) {
|
|
||||||
html += this.formatMetadataField('Modified', metadata.timestamps.modified_human);
|
|
||||||
}
|
|
||||||
processedKeys.add('timestamps');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show extracted info if available
|
|
||||||
if (metadata.extracted_info && Object.keys(metadata.extracted_info).length > 0) {
|
|
||||||
html += `<div class="metadata-section-title">Plot Details</div>`;
|
|
||||||
for (const [key, value] of Object.entries(metadata.extracted_info)) {
|
|
||||||
html += this.formatMetadataField(key, value);
|
|
||||||
}
|
|
||||||
processedKeys.add('extracted_info');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display other fields (excluding generation info unless it's the only data)
|
|
||||||
const otherKeys = Object.keys(metadata).filter(key =>
|
|
||||||
!processedKeys.has(key) && key !== 'generation'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (otherKeys.length > 0) {
|
|
||||||
html += `<div class="metadata-section-title">Additional Information</div>`;
|
|
||||||
for (const key of otherKeys) {
|
|
||||||
html += this.formatMetadataField(key, metadata[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show generation info last if there's no other meaningful data
|
|
||||||
if (processedKeys.size <= 2 && metadata.generation) {
|
|
||||||
html += `<div class="metadata-section-title">Generation Info</div>`;
|
|
||||||
if (metadata.generation.generation_time) {
|
|
||||||
const genDate = new Date(metadata.generation.generation_time);
|
|
||||||
html += this.formatMetadataField('Generated', genDate.toLocaleString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatMetadataField(key, value) {
|
|
||||||
const displayKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
||||||
|
|
||||||
let formattedValue;
|
|
||||||
if (value === null || value === undefined) {
|
|
||||||
formattedValue = '<em>null</em>';
|
|
||||||
} else if (typeof value === 'object') {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
if (value.length <= 3) {
|
|
||||||
formattedValue = value.map(item => `<span class="metadata-tag">${item}</span>`).join(' ');
|
|
||||||
} else {
|
|
||||||
formattedValue = `${value.slice(0, 3).map(item => `<span class="metadata-tag">${item}</span>`).join(' ')} <span class="metadata-more">+${value.length - 3} more</span>`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Show object as compact JSON for small objects, or just key count for large ones
|
|
||||||
const keys = Object.keys(value);
|
|
||||||
if (keys.length <= 3) {
|
|
||||||
formattedValue = '<code>' + JSON.stringify(value) + '</code>';
|
|
||||||
} else {
|
|
||||||
formattedValue = `<em>Object with ${keys.length} properties</em>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Truncate long strings
|
|
||||||
const str = String(value);
|
|
||||||
formattedValue = str.length > 50 ? str.substring(0, 47) + '...' : str;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="metadata-field">
|
|
||||||
<span class="metadata-key">${displayKey}:</span>
|
|
||||||
<span class="metadata-value">${formattedValue}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global instance
|
|
||||||
window.metadataPopup = new MetadataPopup();
|
|
||||||
|
|
||||||
// Global function for template usage
|
|
||||||
window.showMetadataPopup = function(button, plotName, metadata) {
|
|
||||||
window.metadataPopup.showPopup(button, plotName, metadata);
|
|
||||||
};
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
/**
|
|
||||||
* Navigation functionality - breadcrumbs and folder tree
|
|
||||||
*/
|
|
||||||
export class NavigationManager {
|
|
||||||
/**
|
|
||||||
* Build breadcrumb navigation based on current path
|
|
||||||
*/
|
|
||||||
buildBreadcrumb() {
|
|
||||||
const currentPath = window.location.pathname;
|
|
||||||
const pathParts = currentPath.split('/').filter(part => part !== '' && part !== 'index.html');
|
|
||||||
const breadcrumb = document.getElementById('breadcrumb');
|
|
||||||
|
|
||||||
if (!breadcrumb) return;
|
|
||||||
|
|
||||||
if (pathParts.length === 0) {
|
|
||||||
breadcrumb.innerHTML = '<span>🏠 Root</span>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = '<a href="/">🏠 Root</a>';
|
|
||||||
|
|
||||||
for (let i = 0; i < pathParts.length; i++) {
|
|
||||||
const part = pathParts[i];
|
|
||||||
html += '<span class="separator">/</span>';
|
|
||||||
|
|
||||||
if (i === pathParts.length - 1) {
|
|
||||||
html += `<span>${decodeURIComponent(part)}</span>`;
|
|
||||||
} else {
|
|
||||||
const levelsUp = pathParts.length - 1 - i;
|
|
||||||
const relativePath = '../'.repeat(levelsUp) + 'index.html';
|
|
||||||
html += `<a href="${relativePath}">${decodeURIComponent(part)}</a>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
breadcrumb.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build and display the folder tree structure
|
|
||||||
*/
|
|
||||||
async buildFolderTree() {
|
|
||||||
const treeContainer = document.getElementById('folderTree');
|
|
||||||
const currentPath = window.location.pathname;
|
|
||||||
|
|
||||||
if (!treeContainer) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const tree = await this.buildTreeRecursive(currentPath, 0, currentPath);
|
|
||||||
treeContainer.innerHTML = tree;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error building folder tree:', error);
|
|
||||||
treeContainer.innerHTML = '<div class="tree-item">❌ Error loading folder tree</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively build tree structure for folders with collapsed empty directories
|
|
||||||
*/
|
|
||||||
async buildTreeRecursive(path, depth, currentPath, maxDepth = 5) {
|
|
||||||
if (depth > maxDepth) {
|
|
||||||
const indent = ' '.repeat(depth);
|
|
||||||
return `<div class="tree-item">${indent}└─ ...</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(path);
|
|
||||||
const htmlContent = await response.text();
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(htmlContent, 'text/html');
|
|
||||||
|
|
||||||
const subdirs = doc.querySelectorAll('h2 + ul li a');
|
|
||||||
const items = doc.querySelectorAll('.grid-item');
|
|
||||||
|
|
||||||
// Check if this is an empty directory (only one subdirectory, no items)
|
|
||||||
if (items.length === 0 && subdirs.length === 1) {
|
|
||||||
const subdir = subdirs[0];
|
|
||||||
const href = subdir.getAttribute('href');
|
|
||||||
if (href) {
|
|
||||||
const baseUrl = path.replace(/\/[^\/]*$/, '/');
|
|
||||||
const subPath = baseUrl + href;
|
|
||||||
|
|
||||||
const collapsedPath = await this.getCollapsedPath(path, subPath);
|
|
||||||
return await this.buildCollapsedTreeItem(collapsedPath, depth, currentPath, maxDepth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normal directory processing
|
|
||||||
const indent = ' '.repeat(depth);
|
|
||||||
const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery';
|
|
||||||
const totalItems = items.length + subdirs.length;
|
|
||||||
const arrow = depth === 0 ? '' : '└─ ';
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
if (path === currentPath) {
|
|
||||||
html += `<div class="tree-item">${indent}${arrow}📁 <span class="tree-current">${folderName}</span> (${totalItems} items)</div>`;
|
|
||||||
} else {
|
|
||||||
html += `<div class="tree-item">${indent}${arrow}📁 <a href="${path}" class="tree-link">${folderName}</a> (${totalItems} items)</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < subdirs.length; i++) {
|
|
||||||
const subdir = subdirs[i];
|
|
||||||
const href = subdir.getAttribute('href');
|
|
||||||
if (href) {
|
|
||||||
const baseUrl = path.replace(/\/[^\/]*$/, '/');
|
|
||||||
const subPath = baseUrl + href;
|
|
||||||
html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
} catch (error) {
|
|
||||||
const indent = ' '.repeat(depth);
|
|
||||||
const arrow = depth === 0 ? '' : '└─ ';
|
|
||||||
const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery';
|
|
||||||
return `<div class="tree-item">${indent}${arrow}📁 ${folderName} (error loading)</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the collapsed path by following empty directories
|
|
||||||
*/
|
|
||||||
async getCollapsedPath(startPath, currentPath) {
|
|
||||||
const pathSegments = [];
|
|
||||||
let path = startPath;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(path);
|
|
||||||
const htmlContent = await response.text();
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(htmlContent, 'text/html');
|
|
||||||
|
|
||||||
const subdirs = doc.querySelectorAll('h2 + ul li a');
|
|
||||||
const items = doc.querySelectorAll('.grid-item');
|
|
||||||
|
|
||||||
const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery';
|
|
||||||
pathSegments.push({ name: folderName, path: path });
|
|
||||||
|
|
||||||
if (items.length > 0 || subdirs.length !== 1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const subdir = subdirs[0];
|
|
||||||
const href = subdir.getAttribute('href');
|
|
||||||
if (href) {
|
|
||||||
const baseUrl = path.replace(/\/[^\/]*$/, '/');
|
|
||||||
path = baseUrl + href;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
segments: pathSegments,
|
|
||||||
finalPath: path
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a collapsed tree item for empty directory chains
|
|
||||||
*/
|
|
||||||
async buildCollapsedTreeItem(collapsedPath, depth, currentPath, maxDepth) {
|
|
||||||
const indent = ' '.repeat(depth);
|
|
||||||
const arrow = depth === 0 ? '' : '└─ ';
|
|
||||||
|
|
||||||
const displayName = collapsedPath.segments.map(seg => seg.name).join(' / ');
|
|
||||||
const finalPath = collapsedPath.finalPath;
|
|
||||||
|
|
||||||
let totalItems = 0;
|
|
||||||
let subdirs = [];
|
|
||||||
try {
|
|
||||||
const response = await fetch(finalPath);
|
|
||||||
const htmlContent = await response.text();
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(htmlContent, 'text/html');
|
|
||||||
|
|
||||||
const subdirElements = doc.querySelectorAll('h2 + ul li a');
|
|
||||||
const items = doc.querySelectorAll('.grid-item');
|
|
||||||
totalItems = items.length + subdirElements.length;
|
|
||||||
|
|
||||||
subdirs = Array.from(subdirElements);
|
|
||||||
} catch (error) {
|
|
||||||
// Handle error case
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
if (finalPath === currentPath) {
|
|
||||||
html += `<div class="tree-item">${indent}${arrow}📁 <span class="tree-current">${displayName}</span> (${totalItems} items)</div>`;
|
|
||||||
} else {
|
|
||||||
html += `<div class="tree-item">${indent}${arrow}📁 <a href="${finalPath}" class="tree-link">${displayName}</a> (${totalItems} items)</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < subdirs.length; i++) {
|
|
||||||
const subdir = subdirs[i];
|
|
||||||
const href = subdir.getAttribute('href');
|
|
||||||
if (href) {
|
|
||||||
const baseUrl = finalPath.replace(/\/[^\/]*$/, '/');
|
|
||||||
const subPath = baseUrl + href;
|
|
||||||
html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
/**
|
|
||||||
* Recent plots sidebar management
|
|
||||||
*/
|
|
||||||
export class RecentPlotsManager {
|
|
||||||
constructor(maxRecentPlots = 20) {
|
|
||||||
this.MAX_RECENT_PLOTS = maxRecentPlots;
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.updateRecentPlotsDisplay();
|
|
||||||
this.trackPlotClicks();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add plot to recent plots list
|
|
||||||
*/
|
|
||||||
addToRecentPlots(plotHref) {
|
|
||||||
const plotName = plotHref.split('/').pop().replace('.pdf', '');
|
|
||||||
const pathParts = plotHref.split('/').filter(p => p !== '' && p !== plotName + '.pdf');
|
|
||||||
const plotPath = pathParts.join(' / ');
|
|
||||||
const thumbUrl = plotHref.replace('.pdf', '.png');
|
|
||||||
|
|
||||||
// Determine the gallery page URL (directory containing the plot)
|
|
||||||
const plotDir = plotHref.substring(0, plotHref.lastIndexOf('/'));
|
|
||||||
const galleryUrl = plotDir + '/index.html';
|
|
||||||
|
|
||||||
const plotInfo = {
|
|
||||||
name: plotName,
|
|
||||||
path: plotPath,
|
|
||||||
href: plotHref,
|
|
||||||
thumbUrl: thumbUrl,
|
|
||||||
galleryUrl: galleryUrl,
|
|
||||||
timestamp: Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
let recentPlots = JSON.parse(localStorage.getItem('recentPlots') || '[]');
|
|
||||||
recentPlots = recentPlots.filter(p => p.href !== plotHref);
|
|
||||||
recentPlots.unshift(plotInfo);
|
|
||||||
recentPlots = recentPlots.slice(0, this.MAX_RECENT_PLOTS);
|
|
||||||
|
|
||||||
localStorage.setItem('recentPlots', JSON.stringify(recentPlots));
|
|
||||||
this.updateRecentPlotsDisplay();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update recent plots sidebar display
|
|
||||||
*/
|
|
||||||
updateRecentPlotsDisplay() {
|
|
||||||
const sidebarContent = document.getElementById('sidebarContent');
|
|
||||||
if (!sidebarContent) return;
|
|
||||||
|
|
||||||
const recentPlots = JSON.parse(localStorage.getItem('recentPlots') || '[]');
|
|
||||||
|
|
||||||
if (recentPlots.length === 0) {
|
|
||||||
sidebarContent.innerHTML = `
|
|
||||||
<div style="text-align: center; color: var(--breadcrumb-color); margin: 2rem 0;">
|
|
||||||
📭 No recent plots yet<br>
|
|
||||||
<small style="opacity: 0.7;">Open some plots to see them here</small>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
recentPlots.forEach(plot => {
|
|
||||||
html += `
|
|
||||||
<div class="recent-plot" onclick="window.recentPlotsManager.openRecentPlot('${plot.galleryUrl || plot.href}', '${plot.name}')" title="${plot.name}">
|
|
||||||
<img src="${plot.thumbUrl}" class="recent-plot-thumb" alt="${plot.name}" />
|
|
||||||
<div class="recent-plot-info">
|
|
||||||
<div class="recent-plot-name">${plot.name}</div>
|
|
||||||
<div class="recent-plot-path">📍 ${plot.path}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
sidebarContent.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open recent plot gallery page and highlight thumbnail
|
|
||||||
*/
|
|
||||||
openRecentPlot(galleryUrl, plotName) {
|
|
||||||
// Navigate to gallery page with plot highlight parameter
|
|
||||||
const url = new URL(galleryUrl, window.location.origin);
|
|
||||||
url.searchParams.set('highlight', plotName);
|
|
||||||
window.location.href = url.toString();
|
|
||||||
this.toggleSidebar();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle recent plots sidebar
|
|
||||||
*/
|
|
||||||
toggleSidebar() {
|
|
||||||
const sidebar = document.getElementById('sidebar');
|
|
||||||
const overlay = document.getElementById('sidebarOverlay');
|
|
||||||
|
|
||||||
if (sidebar) sidebar.classList.toggle('open');
|
|
||||||
if (overlay) overlay.classList.toggle('open');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Track clicks on plot links
|
|
||||||
*/
|
|
||||||
trackPlotClicks() {
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
const link = e.target.closest('a[href$=".pdf"]');
|
|
||||||
if (link) {
|
|
||||||
this.addToRecentPlots(link.href);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
/**
|
|
||||||
* Search functionality
|
|
||||||
*/
|
|
||||||
export class SearchManager {
|
|
||||||
constructor(debounceMs = 300) {
|
|
||||||
this.searchTimeout = null;
|
|
||||||
this.SEARCH_DEBOUNCE_MS = debounceMs;
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize search functionality with debouncing
|
|
||||||
*/
|
|
||||||
init() {
|
|
||||||
const searchBox = document.getElementById('searchBox');
|
|
||||||
const searchResults = document.getElementById('searchResults');
|
|
||||||
|
|
||||||
if (!searchBox || !searchResults) return;
|
|
||||||
|
|
||||||
searchBox.addEventListener('input', (e) => {
|
|
||||||
clearTimeout(this.searchTimeout);
|
|
||||||
const query = e.target.value.trim();
|
|
||||||
|
|
||||||
if (query.length === 0) {
|
|
||||||
searchResults.style.display = 'none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.searchTimeout = setTimeout(() => {
|
|
||||||
this.performSearch(query);
|
|
||||||
}, this.SEARCH_DEBOUNCE_MS);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
if (!searchBox.contains(e.target) && !searchResults.contains(e.target)) {
|
|
||||||
searchResults.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform search across plot names
|
|
||||||
*/
|
|
||||||
async performSearch(query) {
|
|
||||||
const searchResults = document.getElementById('searchResults');
|
|
||||||
if (!searchResults) return;
|
|
||||||
|
|
||||||
searchResults.innerHTML = '<div style="padding: 1rem;">🔍 Searching...</div>';
|
|
||||||
searchResults.style.display = 'block';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const results = await this.searchPlots(query);
|
|
||||||
this.displaySearchResults(results, query);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Search error:', error);
|
|
||||||
searchResults.innerHTML = '<div style="padding: 1rem; color: red;">❌ Search failed</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for plots matching the query
|
|
||||||
*/
|
|
||||||
async searchPlots(query) {
|
|
||||||
const results = [];
|
|
||||||
const visited = new Set();
|
|
||||||
const lowerQuery = query.toLowerCase();
|
|
||||||
|
|
||||||
await this.searchInPage(window.location.pathname, lowerQuery, results, visited);
|
|
||||||
await this.searchRecursive(window.location.pathname, lowerQuery, results, visited, 0, 5);
|
|
||||||
|
|
||||||
return results.slice(0, 20);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for plots in a specific page
|
|
||||||
*/
|
|
||||||
async searchInPage(path, query, results, visited, maxResults = 50) {
|
|
||||||
if (visited.has(path) || results.length >= maxResults) return;
|
|
||||||
visited.add(path);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(path);
|
|
||||||
const html = await response.text();
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
|
||||||
|
|
||||||
const items = doc.querySelectorAll('.grid-item');
|
|
||||||
items.forEach(item => {
|
|
||||||
const nameElement = item.querySelector('.plot-name');
|
|
||||||
const imgElement = item.querySelector('img');
|
|
||||||
const linkElement = item.querySelector('a');
|
|
||||||
|
|
||||||
if (nameElement && imgElement && linkElement) {
|
|
||||||
const name = nameElement.textContent.toLowerCase();
|
|
||||||
if (name.includes(query)) {
|
|
||||||
results.push({
|
|
||||||
name: nameElement.textContent,
|
|
||||||
path: path,
|
|
||||||
href: linkElement.href,
|
|
||||||
imgSrc: imgElement.src,
|
|
||||||
relevance: this.calculateRelevance(name, query)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error searching in', path, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively search in subdirectories
|
|
||||||
*/
|
|
||||||
async searchRecursive(path, query, results, visited, depth, maxDepth) {
|
|
||||||
if (depth >= maxDepth || results.length >= 50) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(path);
|
|
||||||
const html = await response.text();
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
|
||||||
|
|
||||||
const subdirs = doc.querySelectorAll('h2 + ul li a');
|
|
||||||
for (const subdir of subdirs) {
|
|
||||||
const href = subdir.getAttribute('href');
|
|
||||||
if (href) {
|
|
||||||
const baseUrl = path.replace(/\/[^\/]*$/, '/');
|
|
||||||
const subPath = baseUrl + href;
|
|
||||||
await this.searchInPage(subPath, query, results, visited);
|
|
||||||
await this.searchRecursive(subPath, query, results, visited, depth + 1, maxDepth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in recursive search:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate search relevance score
|
|
||||||
*/
|
|
||||||
calculateRelevance(text, query) {
|
|
||||||
const exactMatch = text === query;
|
|
||||||
const startsWith = text.startsWith(query);
|
|
||||||
const wordMatch = text.split(/\s+/).some(word => word.startsWith(query));
|
|
||||||
|
|
||||||
if (exactMatch) return 100;
|
|
||||||
if (startsWith) return 80;
|
|
||||||
if (wordMatch) return 60;
|
|
||||||
return 40;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display search results with highlighting
|
|
||||||
*/
|
|
||||||
displaySearchResults(results, query) {
|
|
||||||
const searchResults = document.getElementById('searchResults');
|
|
||||||
if (!searchResults) return;
|
|
||||||
|
|
||||||
if (results.length === 0) {
|
|
||||||
searchResults.innerHTML = '<div style="padding: 1rem;">📭 No plots found</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
results.sort((a, b) => b.relevance - a.relevance);
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
results.forEach(result => {
|
|
||||||
const highlightedName = this.highlightText(result.name, query);
|
|
||||||
const relativePath = this.getRelativePath(result.path);
|
|
||||||
|
|
||||||
html += `
|
|
||||||
<div class="search-result-item" onclick="window.searchManager.openSearchResult('${result.href}')" title="${result.name}">
|
|
||||||
<div style="display: flex; gap: 0.7rem; align-items: center;">
|
|
||||||
<img src="${result.imgSrc}" style="width: 50px; height: 40px; object-fit: cover; border-radius: 4px;" />
|
|
||||||
<div style="flex: 1; min-width: 0;">
|
|
||||||
<div style="font-weight: 600; margin-bottom: 0.3rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
|
||||||
${highlightedName}
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 0.8rem; color: var(--breadcrumb-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
|
||||||
📍 ${relativePath}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
searchResults.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Highlight search query in text
|
|
||||||
*/
|
|
||||||
highlightText(text, query) {
|
|
||||||
const regex = new RegExp(`(${query})`, 'gi');
|
|
||||||
return text.replace(regex, '<span class="search-highlight">$1</span>');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get relative path for display
|
|
||||||
*/
|
|
||||||
getRelativePath(fullPath) {
|
|
||||||
const parts = fullPath.split('/').filter(p => p !== '' && p !== 'index.html');
|
|
||||||
return parts.length > 0 ? parts.join(' / ') : 'Root';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open search result and track it
|
|
||||||
*/
|
|
||||||
openSearchResult(href) {
|
|
||||||
if (window.recentPlotsManager) {
|
|
||||||
window.recentPlotsManager.addToRecentPlots(href);
|
|
||||||
}
|
|
||||||
window.open(href, '_blank');
|
|
||||||
document.getElementById('searchResults').style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,6 @@ paths:
|
|||||||
gallery:
|
gallery:
|
||||||
plot_root: "gallery"
|
plot_root: "gallery"
|
||||||
png_dpi: 400
|
png_dpi: 400
|
||||||
backup_folder: ""
|
|
||||||
|
|
||||||
ui:
|
ui:
|
||||||
max_recent_plots: 20
|
max_recent_plots: 20
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ gallery:
|
|||||||
# PNG conversion quality
|
# PNG conversion quality
|
||||||
png_dpi: 400
|
png_dpi: 400
|
||||||
|
|
||||||
# Backup folder (leave empty to disable)
|
|
||||||
backup_folder: ""
|
|
||||||
|
|
||||||
# UI Settings
|
# UI Settings
|
||||||
ui:
|
ui:
|
||||||
# Maximum number of recent plots to track
|
# Maximum number of recent plots to track
|
||||||
|
|||||||
@@ -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.*
|
|
||||||
@@ -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.
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
@import url('./metadata.css');
|
@import url('./metadata.css');
|
||||||
@import url('./metadata-section.css');
|
@import url('./metadata-section.css');
|
||||||
@import url('./folder-metadata.css');
|
@import url('./folder-metadata.css');
|
||||||
@import url('./export.css');
|
|
||||||
|
|
||||||
/* View controls - must come after grid.css to override */
|
/* View controls - must come after grid.css to override */
|
||||||
@import url('./view-controls.css');
|
@import url('./view-controls.css');
|
||||||
|
|||||||
@@ -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 = `
|
|
||||||
<div class="selection-checkbox">
|
|
||||||
<span class="checkbox-icon">☐</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
item.appendChild(overlay);
|
|
||||||
|
|
||||||
// Add click handler for selection
|
|
||||||
overlay.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.togglePlotSelection(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle selection mode
|
|
||||||
*/
|
|
||||||
toggleSelectionMode() {
|
|
||||||
const body = document.body;
|
|
||||||
const isSelectionMode = body.classList.contains('selection-mode');
|
|
||||||
|
|
||||||
if (isSelectionMode) {
|
|
||||||
this.exitSelectionMode();
|
|
||||||
} else {
|
|
||||||
this.enterSelectionMode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enter selection mode
|
|
||||||
*/
|
|
||||||
enterSelectionMode() {
|
|
||||||
document.body.classList.add('selection-mode');
|
|
||||||
document.getElementById('exportBtn').style.display = 'block';
|
|
||||||
document.getElementById('selectionCounter').style.display = 'block';
|
|
||||||
this.updateSelectionCounter();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exit selection mode
|
|
||||||
*/
|
|
||||||
exitSelectionMode() {
|
|
||||||
const wasInSelectionMode = document.body.classList.contains('selection-mode');
|
|
||||||
|
|
||||||
document.body.classList.remove('selection-mode');
|
|
||||||
document.getElementById('exportBtn').style.display = 'none';
|
|
||||||
document.getElementById('selectionCounter').style.display = 'none';
|
|
||||||
this.clearSelection();
|
|
||||||
|
|
||||||
// Show message if user was actually in selection mode
|
|
||||||
if (wasInSelectionMode) {
|
|
||||||
this.showMessage('Exited selection mode', 'info');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle plot selection
|
|
||||||
*/
|
|
||||||
togglePlotSelection(item) {
|
|
||||||
const plotName = this.getPlotName(item);
|
|
||||||
const plotPath = this.getPlotPath(item);
|
|
||||||
|
|
||||||
if (this.selectedPlots.has(plotName)) {
|
|
||||||
this.selectedPlots.delete(plotName);
|
|
||||||
item.classList.remove('selected');
|
|
||||||
item.querySelector('.checkbox-icon').textContent = '☐';
|
|
||||||
} else {
|
|
||||||
if (this.selectedPlots.size >= this.maxPlots) {
|
|
||||||
this.showMessage(`Maximum ${this.maxPlots} plots can be selected`, 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.selectedPlots.add(plotName);
|
|
||||||
item.classList.add('selected');
|
|
||||||
item.querySelector('.checkbox-icon').textContent = '☑';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateSelectionCounter();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get plot name from grid item
|
|
||||||
*/
|
|
||||||
getPlotName(item) {
|
|
||||||
const plotName = item.querySelector('.plot-name');
|
|
||||||
return plotName ? plotName.textContent.trim() : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get plot PDF path from grid item
|
|
||||||
*/
|
|
||||||
getPlotPath(item) {
|
|
||||||
const link = item.querySelector('a[href$=".pdf"]');
|
|
||||||
return link ? link.href : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update selection counter
|
|
||||||
*/
|
|
||||||
updateSelectionCounter() {
|
|
||||||
const counter = document.getElementById('selectionCounter');
|
|
||||||
if (counter) {
|
|
||||||
counter.textContent = `${this.selectedPlots.size}/${this.maxPlots} selected`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportBtn = document.getElementById('exportBtn');
|
|
||||||
if (exportBtn) {
|
|
||||||
exportBtn.disabled = this.selectedPlots.size === 0;
|
|
||||||
exportBtn.style.opacity = this.selectedPlots.size === 0 ? '0.5' : '1';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all selections
|
|
||||||
*/
|
|
||||||
clearSelection() {
|
|
||||||
this.selectedPlots.clear();
|
|
||||||
document.querySelectorAll('.grid-item.selected').forEach(item => {
|
|
||||||
item.classList.remove('selected');
|
|
||||||
const checkbox = item.querySelector('.checkbox-icon');
|
|
||||||
if (checkbox) checkbox.textContent = '☐';
|
|
||||||
});
|
|
||||||
this.updateSelectionCounter();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export selected plots to merged PDF
|
|
||||||
*/
|
|
||||||
async exportSelectedPlots() {
|
|
||||||
if (this.selectedPlots.size === 0) {
|
|
||||||
this.showMessage('No plots selected', 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const plotPaths = Array.from(this.selectedPlots).map(plotName => {
|
|
||||||
const item = Array.from(document.querySelectorAll('.grid-item'))
|
|
||||||
.find(item => this.getPlotName(item) === plotName);
|
|
||||||
return this.getPlotPath(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.showMessage('Preparing export...', 'info');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.createMergedPDF(plotPaths);
|
|
||||||
} catch (error) {
|
|
||||||
this.showMessage('Export failed: ' + error.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create merged PDF using Python script
|
|
||||||
*/
|
|
||||||
async createMergedPDF(plotPaths) {
|
|
||||||
// Convert file:// URLs to actual paths
|
|
||||||
const actualPaths = plotPaths.map(url => {
|
|
||||||
if (url.startsWith('file://')) {
|
|
||||||
return url.substring(7); // Remove 'file://' prefix
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
});
|
|
||||||
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
|
|
||||||
const outputName = `merged_plots_${timestamp}.pdf`;
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
plots: actualPaths,
|
|
||||||
layout: this.calculateLayout(actualPaths.length),
|
|
||||||
output_name: outputName
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate a unique temporary filename
|
|
||||||
const tempFileName = `export_request_${Date.now()}.json`;
|
|
||||||
|
|
||||||
// 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 = `
|
|
||||||
<div class="export-instructions">
|
|
||||||
<h3>🚀 Export Selected Plots</h3>
|
|
||||||
<p>Run the following command in your terminal to export the selected plots:</p>
|
|
||||||
|
|
||||||
<div class="export-command-container">
|
|
||||||
<div class="export-command">
|
|
||||||
<code id="exportCommand">${fullCommand}</code>
|
|
||||||
</div>
|
|
||||||
<div class="export-actions">
|
|
||||||
<button onclick="this.copyCommand()" class="copy-btn" title="Copy command to clipboard">
|
|
||||||
📋 Copy Command
|
|
||||||
</button>
|
|
||||||
<button onclick="this.copyJSON()" class="copy-btn" title="Copy JSON only">
|
|
||||||
📄 Copy JSON
|
|
||||||
</button>
|
|
||||||
<button onclick="this.close()" class="close-btn">
|
|
||||||
✕ Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="export-details">
|
|
||||||
<h4>📋 Command Breakdown:</h4>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Creates temporary file:</strong> <code>${tempFilePath}</code></li>
|
|
||||||
<li><strong>Runs export script:</strong> <code>python export_plots.py</code></li>
|
|
||||||
<li><strong>Output file:</strong> Will be saved in the work directory</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="export-tips">
|
|
||||||
<h4>💡 Tips:</h4>
|
|
||||||
<ul>
|
|
||||||
<li>The temporary JSON file will be automatically cleaned up after successful export</li>
|
|
||||||
<li>Use <kbd>Esc</kbd> to exit selection mode</li>
|
|
||||||
<li>Press <kbd>Ctrl+E</kbd> to toggle selection mode</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const overlay = document.createElement('div');
|
|
||||||
overlay.className = 'export-overlay';
|
|
||||||
overlay.innerHTML = instructions;
|
|
||||||
|
|
||||||
// Add methods to the overlay for button handlers
|
|
||||||
overlay.copyCommand = function() {
|
|
||||||
navigator.clipboard.writeText(fullCommand).then(() => {
|
|
||||||
this.showCopyFeedback('Command copied to clipboard!');
|
|
||||||
}).catch(() => {
|
|
||||||
this.showCopyFeedback('Failed to copy. Please select and copy manually.', 'error');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
overlay.copyJSON = function() {
|
|
||||||
navigator.clipboard.writeText(requestData).then(() => {
|
|
||||||
this.showCopyFeedback('JSON copied to clipboard!');
|
|
||||||
}).catch(() => {
|
|
||||||
this.showCopyFeedback('Failed to copy. Please select and copy manually.', 'error');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
overlay.close = function() {
|
|
||||||
this.remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
overlay.showCopyFeedback = function(message, type = 'success') {
|
|
||||||
const feedback = document.createElement('div');
|
|
||||||
feedback.className = `copy-feedback copy-feedback-${type}`;
|
|
||||||
feedback.textContent = message;
|
|
||||||
this.appendChild(feedback);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (feedback.parentNode) {
|
|
||||||
feedback.parentNode.removeChild(feedback);
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
|
|
||||||
// Close on ESC key
|
|
||||||
const handleEscape = (e) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
overlay.remove();
|
|
||||||
document.removeEventListener('keydown', handleEscape);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('keydown', handleEscape);
|
|
||||||
|
|
||||||
// Close on clicking outside
|
|
||||||
overlay.addEventListener('click', (e) => {
|
|
||||||
if (e.target === overlay) {
|
|
||||||
overlay.remove();
|
|
||||||
document.removeEventListener('keydown', handleEscape);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download the PDF blob
|
|
||||||
*/
|
|
||||||
downloadPDF(blob) {
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = `merged_plots_${new Date().toISOString().split('T')[0]}.pdf`;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show temporary message to user
|
|
||||||
*/
|
|
||||||
showMessage(text, type = 'info') {
|
|
||||||
// Remove existing message
|
|
||||||
const existing = document.querySelector('.export-message');
|
|
||||||
if (existing) existing.remove();
|
|
||||||
|
|
||||||
const message = document.createElement('div');
|
|
||||||
message.className = `export-message export-message-${type}`;
|
|
||||||
message.textContent = text;
|
|
||||||
|
|
||||||
document.body.appendChild(message);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (message.parentNode) {
|
|
||||||
message.parentNode.removeChild(message);
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add these methods to ExportManager if not present
|
|
||||||
ExportManager.prototype.isSelectionModeActive = function() {
|
|
||||||
return document.body.classList.contains('selection-mode');
|
|
||||||
};
|
|
||||||
ExportManager.prototype.exitSelectionMode = function() {
|
|
||||||
document.body.classList.remove('selection-mode');
|
|
||||||
if (typeof this.clearSelection === 'function') {
|
|
||||||
this.clearSelection();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ensure a single global instance
|
|
||||||
window.exportManager = window.exportManager || new ExportManager();
|
|
||||||
|
|
||||||
// Listen for ESC key globally to exit selection mode
|
|
||||||
// (This will work even if focus is not on a plot)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape' && window.exportManager && window.exportManager.isSelectionModeActive()) {
|
|
||||||
window.exportManager.exitSelectionMode();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Attach improved export logic to export button
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const exportBtn = document.getElementById('exportBtn');
|
|
||||||
if (exportBtn) {
|
|
||||||
exportBtn.addEventListener('click', function() {
|
|
||||||
window.exportManager.exportSelectedPlots();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -167,7 +167,6 @@ class GalleryConfig:
|
|||||||
plot_root: str = GalleryDefaults.plot_root
|
plot_root: str = GalleryDefaults.plot_root
|
||||||
cache_enabled: bool = GalleryDefaults.cache_enabled
|
cache_enabled: bool = GalleryDefaults.cache_enabled
|
||||||
inherit_from_parent: bool = GalleryDefaults.inherit_from_parent
|
inherit_from_parent: bool = GalleryDefaults.inherit_from_parent
|
||||||
backup_folder: str = ""
|
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
if isinstance(self.web_folder, str):
|
if isinstance(self.web_folder, str):
|
||||||
@@ -215,7 +214,6 @@ class GalleryConfig:
|
|||||||
plot_root=gallery_cfg.get("plot_root", GalleryDefaults.plot_root),
|
plot_root=gallery_cfg.get("plot_root", GalleryDefaults.plot_root),
|
||||||
cache_enabled=metadata_cfg.get("cache_enabled", GalleryDefaults.cache_enabled),
|
cache_enabled=metadata_cfg.get("cache_enabled", GalleryDefaults.cache_enabled),
|
||||||
inherit_from_parent=metadata_cfg.get("inherit_from_parent", GalleryDefaults.inherit_from_parent),
|
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:
|
def to_yaml(self, yaml_file: Union[str, Path]) -> None:
|
||||||
@@ -229,7 +227,6 @@ class GalleryConfig:
|
|||||||
"gallery": {
|
"gallery": {
|
||||||
"plot_root": self.plot_root,
|
"plot_root": self.plot_root,
|
||||||
"png_dpi": self.png_dpi,
|
"png_dpi": self.png_dpi,
|
||||||
"backup_folder": self.backup_folder,
|
|
||||||
},
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
"max_recent_plots": 20,
|
"max_recent_plots": 20,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
gallery:
|
gallery:
|
||||||
backup_folder: ''
|
|
||||||
plot_root: gallery
|
plot_root: gallery
|
||||||
png_dpi: 400
|
png_dpi: 400
|
||||||
metadata:
|
metadata:
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ CONFIG_FIELDS = [
|
|||||||
("web-folder", "paths.web_folder", True),
|
("web-folder", "paths.web_folder", True),
|
||||||
("plot-root", "gallery.plot_root", False),
|
("plot-root", "gallery.plot_root", False),
|
||||||
("png-dpi", "gallery.png_dpi", False),
|
("png-dpi", "gallery.png_dpi", False),
|
||||||
("backup-folder", "gallery.backup_folder", False),
|
|
||||||
("cache-enabled", "metadata.cache_enabled", False),
|
("cache-enabled", "metadata.cache_enabled", False),
|
||||||
("inherit-meta", "metadata.inherit_from_parent", False),
|
("inherit-meta", "metadata.inherit_from_parent", False),
|
||||||
]
|
]
|
||||||
@@ -250,9 +249,6 @@ class GalleryTUI(App):
|
|||||||
with Horizontal(classes="field-row"):
|
with Horizontal(classes="field-row"):
|
||||||
yield Label("PNG DPI")
|
yield Label("PNG DPI")
|
||||||
yield Input(id="png-dpi", placeholder="400")
|
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"):
|
with Horizontal(classes="field-row"):
|
||||||
yield Label("Cache metadata")
|
yield Label("Cache metadata")
|
||||||
yield Input(id="cache-enabled", placeholder="true")
|
yield Input(id="cache-enabled", placeholder="true")
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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.")
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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)
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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
|
|
||||||
@@ -1,363 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>{{ title }}</title>
|
|
||||||
<link rel="stylesheet" href="{{ assets_path }}/css/main.css">
|
|
||||||
|
|
||||||
<!-- MathJax for LaTeX rendering -->
|
|
||||||
<script>
|
|
||||||
MathJax = {
|
|
||||||
tex: {
|
|
||||||
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
|
||||||
displayMath: [['$$', '$$'], ['\\[', '\\]']]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Main Content -->
|
|
||||||
<h1>{{ title }}</h1>
|
|
||||||
|
|
||||||
<!-- Search Bar -->
|
|
||||||
<div class="search-container">
|
|
||||||
<input type="text" class="search-box" id="searchBox" placeholder="Search plots..." />
|
|
||||||
<span class="search-icon">🔍</span>
|
|
||||||
<div class="search-results" id="searchResults"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Breadcrumb Navigation -->
|
|
||||||
<div class="breadcrumb" id="breadcrumb"></div>
|
|
||||||
|
|
||||||
<!-- Navigation Buttons -->
|
|
||||||
<div class="navigation">
|
|
||||||
<button class="nav-btn" onclick="window.history.back()">
|
|
||||||
← Back
|
|
||||||
</button>
|
|
||||||
{% if relpath != "." %}
|
|
||||||
<a href="../index.html" class="nav-btn">
|
|
||||||
↑ Parent Directory
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Folder Tree -->
|
|
||||||
<div class="folder-tree" id="folderTree"></div>
|
|
||||||
|
|
||||||
<!-- Folder Metadata Section -->
|
|
||||||
{% if folder_metadata %}
|
|
||||||
<div class="metadata-section">
|
|
||||||
<button class="metadata-toggle-btn" onclick="toggleMetadataSection()">
|
|
||||||
<span class="metadata-icon">📋</span>
|
|
||||||
<span class="metadata-label">Folder Information</span>
|
|
||||||
<span class="metadata-arrow" id="metadataArrow">▼</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="metadata-content" id="metadataContent" style="display: none;">
|
|
||||||
<div class="metadata-header">
|
|
||||||
<div class="metadata-file-info">
|
|
||||||
<span class="file-path-label">📁 Metadata file:</span>
|
|
||||||
<code class="file-path" id="metadata-file-path">{{ metadata_file_path }}</code>
|
|
||||||
<button class="copy-path-btn" onclick="copyMetadataPath()" title="Copy path to clipboard">
|
|
||||||
📋 Copy
|
|
||||||
</button>
|
|
||||||
<span class="tip-icon" title="Tip: Create this file in the source directory to add folder-level metadata that will be inherited by all plots in this folder and its subdirectories. Supports both YAML (.yaml/.yml) and JSON (.json) formats.">
|
|
||||||
💡
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="metadata-grid">
|
|
||||||
{% for key, value in folder_metadata.items() %}
|
|
||||||
<div class="metadata-item">
|
|
||||||
<span class="metadata-key">{{ key }}:</span>
|
|
||||||
<span class="metadata-value">
|
|
||||||
{% if value is string and (value.startswith('http://') or value.startswith('https://')) %}
|
|
||||||
<a href="{{ value }}" target="_blank" rel="noopener noreferrer">{{ value }}</a>
|
|
||||||
{% elif value is string and '$$' in value %}
|
|
||||||
<span class="latex-content">{{ value }}</span>
|
|
||||||
{% elif value is string and value|length > 100 %}
|
|
||||||
<span class="metadata-long-text">{{ value[:100] }}...</span>
|
|
||||||
<button class="metadata-expand" onclick="expandText(this)">Show more</button>
|
|
||||||
<span class="metadata-full-text" style="display: none;">{{ value }}</span>
|
|
||||||
{% elif value is iterable and value is not string and value is not mapping %}
|
|
||||||
<div class="metadata-yaml-list">
|
|
||||||
{% for item in value %}
|
|
||||||
<div class="yaml-list-item">- {{ item }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% elif value is mapping %}
|
|
||||||
<div class="metadata-yaml-object">
|
|
||||||
{% for subkey, subvalue in value.items() %}
|
|
||||||
<div class="yaml-object-item">
|
|
||||||
<span class="yaml-key">{{ subkey }}:</span>
|
|
||||||
{% if subvalue is iterable and subvalue is not string and subvalue is not mapping %}
|
|
||||||
<div class="yaml-nested-list">
|
|
||||||
{% for nested_item in subvalue %}
|
|
||||||
<div class="yaml-nested-item">- {{ nested_item }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% elif subvalue is mapping %}
|
|
||||||
<div class="yaml-nested-object">
|
|
||||||
{% for nested_key, nested_value in subvalue.items() %}
|
|
||||||
<div class="yaml-nested-item">{{ nested_key }}: {{ nested_value }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<span class="yaml-value"> {{ subvalue }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{{ value }}
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- View Toggle Controls - Only show if there are plot items -->
|
|
||||||
{% if items %}
|
|
||||||
<div class="controls-container">
|
|
||||||
<div class="sort-controls">
|
|
||||||
<label class="sort-label">Sort by:</label>
|
|
||||||
<button class="sort-btn active" data-sort="name" title="Sort by Name">
|
|
||||||
📝 Name
|
|
||||||
</button>
|
|
||||||
<button class="sort-btn" data-sort="time" title="Sort by Creation Time">
|
|
||||||
🕒 Time
|
|
||||||
</button>
|
|
||||||
<button class="sort-order-btn" data-order="asc" title="Sort Order">
|
|
||||||
↑
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="view-controls">
|
|
||||||
<button class="view-btn active" data-view="grid" title="Grid View">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16">
|
|
||||||
<rect x="1" y="1" width="6" height="6" fill="currentColor"/>
|
|
||||||
<rect x="9" y="1" width="6" height="6" fill="currentColor"/>
|
|
||||||
<rect x="1" y="9" width="6" height="6" fill="currentColor"/>
|
|
||||||
<rect x="9" y="9" width="6" height="6" fill="currentColor"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button class="view-btn" data-view="list-large" title="Large List View">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16">
|
|
||||||
<rect x="1" y="2" width="4" height="3" fill="currentColor"/>
|
|
||||||
<rect x="7" y="2" width="8" height="1" fill="currentColor"/>
|
|
||||||
<rect x="7" y="4" width="6" height="1" fill="currentColor"/>
|
|
||||||
<rect x="1" y="7" width="4" height="3" fill="currentColor"/>
|
|
||||||
<rect x="7" y="7" width="8" height="1" fill="currentColor"/>
|
|
||||||
<rect x="7" y="9" width="6" height="1" fill="currentColor"/>
|
|
||||||
<rect x="1" y="12" width="4" height="3" fill="currentColor"/>
|
|
||||||
<rect x="7" y="12" width="8" height="1" fill="currentColor"/>
|
|
||||||
<rect x="7" y="14" width="6" height="1" fill="currentColor"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button class="view-btn" data-view="list-compact" title="Compact List View">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16">
|
|
||||||
<rect x="1" y="3" width="14" height="1" fill="currentColor"/>
|
|
||||||
<rect x="1" y="6" width="14" height="1" fill="currentColor"/>
|
|
||||||
<rect x="1" y="9" width="14" height="1" fill="currentColor"/>
|
|
||||||
<rect x="1" y="12" width="14" height="1" fill="currentColor"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Plot Container -->
|
|
||||||
<div class="plot-container grid-view" id="plotContainer">
|
|
||||||
{% for item in items %}
|
|
||||||
<div class="plot-item grid-item"
|
|
||||||
data-name="{{ item.name }}"
|
|
||||||
data-time="{{ item.creation_time|default(0) }}">
|
|
||||||
<a href="{{ item.pdf_href }}" class="plot-link">
|
|
||||||
<img src="{{ item.png_href }}" alt="{{ item.name }}" class="plot-thumbnail">
|
|
||||||
</a>
|
|
||||||
<div class="plot-info">
|
|
||||||
<div class="plot-name" title="{{ item.name }}">{{ item.name }}</div>
|
|
||||||
<div class="plot-date" title="Created: {{ item.creation_time|default(0)|int|datetime_from_timestamp|strftime('%Y-%m-%d %H:%M') if item.creation_time and item.creation_time|int > 0 else 'Unknown' }}">
|
|
||||||
{% if item.creation_time and item.creation_time|int > 0 %}
|
|
||||||
{{ item.creation_time|int|datetime_from_timestamp|strftime('%Y-%m-%d') }}
|
|
||||||
{% else %}
|
|
||||||
Unknown
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Subdirectories -->
|
|
||||||
{% if subdirs %}
|
|
||||||
<h2>Subdirectories</h2>
|
|
||||||
<ul>
|
|
||||||
{% for sub in subdirs %}
|
|
||||||
<li><a href="{{ sub }}/index.html">📁 {{ sub }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Recent Plots Sidebar -->
|
|
||||||
<div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleSidebar()"></div>
|
|
||||||
<div class="sidebar" id="sidebar">
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<h3 class="sidebar-title">Recent Plots</h3>
|
|
||||||
<button class="sidebar-close" onclick="toggleSidebar()">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="sidebar-content" id="sidebarContent">
|
|
||||||
<div style="text-align: center; color: var(--breadcrumb-color); margin: 2rem 0;">
|
|
||||||
No recent plots yet
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Floating Action Buttons -->
|
|
||||||
<div class="floating-buttons">
|
|
||||||
<button class="floating-btn sidebar-toggle" onclick="toggleSidebar()" id="sidebarToggle" title="Recent Plots (Ctrl+R)">
|
|
||||||
📋
|
|
||||||
</button>
|
|
||||||
<button class="floating-btn compare-toggle" onclick="app.toggleCompareMode()" id="compareToggle" title="Compare Plots (Ctrl+C)">
|
|
||||||
⚖️
|
|
||||||
</button>
|
|
||||||
<button class="floating-btn theme-toggle" onclick="toggleTheme()" id="themeToggle" title="Toggle Theme (Ctrl+T)">
|
|
||||||
☀️
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Keyboard Shortcuts Help -->
|
|
||||||
<div class="shortcuts-help" id="shortcutsHelp">
|
|
||||||
<h4>Keyboard Shortcuts</h4>
|
|
||||||
<div class="shortcut-item">
|
|
||||||
<span>Search</span>
|
|
||||||
<span class="shortcut-key">Ctrl+K</span>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-item">
|
|
||||||
<span>Recent plots</span>
|
|
||||||
<span class="shortcut-key">Ctrl+R</span>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-item">
|
|
||||||
<span>Compare plots</span>
|
|
||||||
<span class="shortcut-key">Ctrl+C</span>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-item">
|
|
||||||
<span>Export plots</span>
|
|
||||||
<span class="shortcut-key">Ctrl+E</span>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-item">
|
|
||||||
<span>Exit selection mode</span>
|
|
||||||
<span class="shortcut-key">Esc</span>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-item">
|
|
||||||
<span>Toggle theme</span>
|
|
||||||
<span class="shortcut-key">Ctrl+T</span>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-item">
|
|
||||||
<span>Toggle view</span>
|
|
||||||
<span class="shortcut-key">Ctrl+V</span>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-item">
|
|
||||||
<span>Sort by name</span>
|
|
||||||
<span class="shortcut-key">Ctrl+N</span>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-item">
|
|
||||||
<span>Sort by time</span>
|
|
||||||
<span class="shortcut-key">Ctrl+M</span>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-item">
|
|
||||||
<span>Toggle sort order</span>
|
|
||||||
<span class="shortcut-key">Ctrl+O</span>
|
|
||||||
</div>
|
|
||||||
<div class="shortcut-item">
|
|
||||||
<span>Help</span>
|
|
||||||
<span class="shortcut-key">?</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Gallery Statistics -->
|
|
||||||
<div class="gallery-stats" id="galleryStats">
|
|
||||||
<div class="stats-item">
|
|
||||||
<span class="stats-label">📊 Files:</span>
|
|
||||||
<span class="stats-value" id="fileCount">0</span>
|
|
||||||
</div>
|
|
||||||
<div class="stats-item">
|
|
||||||
<span class="stats-label">📁 Folders:</span>
|
|
||||||
<span class="stats-value" id="folderCount">0</span>
|
|
||||||
</div>
|
|
||||||
<div class="stats-item">
|
|
||||||
<span class="stats-label">💾 Size:</span>
|
|
||||||
<span class="stats-value" id="totalSize">0 KB</span>
|
|
||||||
</div>
|
|
||||||
<div class="stats-item">
|
|
||||||
<span class="stats-label">🕒 Updated:</span>
|
|
||||||
<span class="stats-value" id="lastUpdated">Now</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Plot Comparison Overlay -->
|
|
||||||
<div class="comparison-overlay" id="comparisonOverlay">
|
|
||||||
<div class="comparison-container">
|
|
||||||
<div class="comparison-header">
|
|
||||||
<h2 class="comparison-title">Plot Comparison</h2>
|
|
||||||
<button class="comparison-close" onclick="app.closeComparison()" title="Close Comparison (Esc)">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="comparison-content">
|
|
||||||
<div class="comparison-panel">
|
|
||||||
<div class="comparison-panel-header">
|
|
||||||
<span class="comparison-panel-title" id="leftPlotTitle">Plot A</span>
|
|
||||||
<button class="comparison-replace-btn" onclick="app.replacePlot('left')" id="leftReplaceBtn">Replace</button>
|
|
||||||
</div>
|
|
||||||
<div class="comparison-panel-content">
|
|
||||||
<div class="comparison-plot-container" id="leftPlotContainer">
|
|
||||||
<div class="comparison-placeholder" onclick="app.selectPlotForComparison('left')">
|
|
||||||
📊 Click to select first plot
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="comparison-panel">
|
|
||||||
<div class="comparison-panel-header">
|
|
||||||
<span class="comparison-panel-title" id="rightPlotTitle">Plot B</span>
|
|
||||||
<button class="comparison-replace-btn" onclick="app.replacePlot('right')" id="rightReplaceBtn">Replace</button>
|
|
||||||
</div>
|
|
||||||
<div class="comparison-panel-content">
|
|
||||||
<div class="comparison-plot-container" id="rightPlotContainer">
|
|
||||||
<div class="comparison-placeholder" onclick="app.selectPlotForComparison('right')">
|
|
||||||
📊 Click to select second plot
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Configuration for JavaScript -->
|
|
||||||
<script>
|
|
||||||
// Configuration object for the gallery app
|
|
||||||
window.galleryConfig = {
|
|
||||||
searchDebounceMs: {{ ui.search_debounce_ms|default(300) }},
|
|
||||||
maxRecentPlots: {{ ui.max_recent_plots|default(20) }},
|
|
||||||
workDir: "{{ paths.work_dir }}",
|
|
||||||
stats: {% if stats %}{{ stats|tojson }}{% else %}null{% endif %}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Metadata Popup Script -->
|
|
||||||
<script src="{{ assets_path }}/js/metadata-popup.js"></script>
|
|
||||||
|
|
||||||
<!-- Metadata Section Script -->
|
|
||||||
<script src="{{ assets_path }}/js/metadata-section.js"></script>
|
|
||||||
|
|
||||||
<!-- Folder Metadata Script -->
|
|
||||||
<script src="{{ assets_path }}/js/folder-metadata.js"></script>
|
|
||||||
|
|
||||||
<!-- Main JavaScript Application -->
|
|
||||||
<script type="module" src="{{ assets_path }}/js/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
-235
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
@@ -28,7 +28,6 @@ def test_gallery_config_defaults():
|
|||||||
assert cfg.plot_root == GalleryDefaults.plot_root
|
assert cfg.plot_root == GalleryDefaults.plot_root
|
||||||
assert cfg.cache_enabled == GalleryDefaults.cache_enabled
|
assert cfg.cache_enabled == GalleryDefaults.cache_enabled
|
||||||
assert cfg.inherit_from_parent == GalleryDefaults.inherit_from_parent
|
assert cfg.inherit_from_parent == GalleryDefaults.inherit_from_parent
|
||||||
assert cfg.backup_folder == ""
|
|
||||||
|
|
||||||
|
|
||||||
def test_gallery_config_sources_from_dicts():
|
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):
|
def test_gallery_config_from_yaml(tmp_path):
|
||||||
yaml_content = {
|
yaml_content = {
|
||||||
"paths": {"web_folder": "/test/web"},
|
"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},
|
"metadata": {"cache_enabled": False, "inherit_from_parent": False},
|
||||||
"sources": [
|
"sources": [
|
||||||
{"name": "source1", "path": "/path1"},
|
{"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.web_folder == Path("/test/web")
|
||||||
assert cfg.plot_root == "test_plots"
|
assert cfg.plot_root == "test_plots"
|
||||||
assert cfg.png_dpi == 200
|
assert cfg.png_dpi == 200
|
||||||
assert cfg.backup_folder == "test_backups"
|
|
||||||
assert cfg.cache_enabled is False
|
assert cfg.cache_enabled is False
|
||||||
assert cfg.inherit_from_parent is False
|
assert cfg.inherit_from_parent is False
|
||||||
assert len(cfg.sources) == 2
|
assert len(cfg.sources) == 2
|
||||||
@@ -106,7 +104,6 @@ def test_gallery_config_to_yaml_round_trip(tmp_path):
|
|||||||
sources=[{"name": "test", "path": "/test"}],
|
sources=[{"name": "test", "path": "/test"}],
|
||||||
plot_root="plots",
|
plot_root="plots",
|
||||||
png_dpi=300,
|
png_dpi=300,
|
||||||
backup_folder="backups",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
yaml_file = tmp_path / "output_config.yaml"
|
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.web_folder == cfg.web_folder
|
||||||
assert reloaded.plot_root == cfg.plot_root
|
assert reloaded.plot_root == cfg.plot_root
|
||||||
assert reloaded.png_dpi == cfg.png_dpi
|
assert reloaded.png_dpi == cfg.png_dpi
|
||||||
assert reloaded.backup_folder == cfg.backup_folder
|
|
||||||
assert reloaded.sources[0].name == "test"
|
assert reloaded.sources[0].name == "test"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user