From 2020ac48836f997b1424c0d70cc1baa8776d4b99 Mon Sep 17 00:00:00 2001 From: Lars Bogner Date: Thu, 23 Jul 2026 13:50:32 +0200 Subject: [PATCH] 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. --- .vscode/settings.json | 6 - README.md | 5 +- __pycache__/config.cpython-312.pyc | Bin 5718 -> 0 bytes assets/css/base.css | 71 ---- assets/css/comparison.css | 180 ---------- assets/css/export.css | 440 ------------------------- assets/css/floating-elements.css | 112 ------- assets/css/folder-metadata.css | 179 ---------- assets/css/folder-tree.css | 40 --- assets/css/grid.css | 106 ------ assets/css/main.css | 33 -- assets/css/metadata-section.css | 383 ---------------------- assets/css/metadata.css | 180 ---------- assets/css/navigation.css | 57 ---- assets/css/responsive.css | 59 ---- assets/css/search.css | 72 ---- assets/css/sidebar.css | 121 ------- assets/css/sort-controls.css | 88 ----- assets/css/sort-debug.css | 0 assets/css/stats.css | 59 ---- assets/css/variables.css | 33 -- assets/css/view-controls.css | 404 ----------------------- assets/css/view-override.css | 54 --- assets/js/comparison-manager.js | 180 ---------- assets/js/export-manager.js | 448 ------------------------- assets/js/folder-metadata.js | 81 ----- assets/js/gallery-app.js | 73 ----- assets/js/keyboard-manager.js | 130 -------- assets/js/main.js | 27 -- assets/js/metadata-popup.js | 212 ------------ assets/js/metadata-section.js | 142 -------- assets/js/navigation-manager.js | 208 ------------ assets/js/recent-plots-manager.js | 114 ------- assets/js/search-manager.js | 217 ------------ assets/js/sort-manager.js | 197 ----------- assets/js/stats-manager.js | 54 --- assets/js/theme-manager.js | 43 --- assets/js/utils.js | 107 ------ assets/js/view-manager.js | 192 ----------- config.lbogner.yaml | 1 - config.yaml | 3 - docs/COVERAGE_TESTING.md | 275 ---------------- docs/SORT_IMPLEMENTATION.md | 136 -------- gallery/assets/css/export.css | 440 ------------------------- gallery/assets/css/main.css | 1 - gallery/assets/css/sort-debug.css | 0 gallery/assets/js/export-manager.js | 444 ------------------------- gallery/config/__init__.py | 3 - gallery/config/config.yaml | 1 - gallery/tui.py | 4 - gallery/utils/backup.py | 37 --- generate_gallery.py | 489 ---------------------------- orchestration/backup.py | 23 -- orchestration/config.py | 147 --------- orchestration/logger.py | 250 -------------- orchestration/metadata.py | 159 --------- orchestration/pdf_export.py | 205 ------------ python/add_creation_time.py | 92 ------ python/add_metadata.py | 45 --- templates/gallery.html | 363 --------------------- test.ipynb | 235 ------------- test_logger.py | 0 tests/test_backup.py | 137 -------- tests/test_config.py | 6 +- 64 files changed, 3 insertions(+), 8600 deletions(-) delete mode 100644 .vscode/settings.json delete mode 100644 __pycache__/config.cpython-312.pyc delete mode 100644 assets/css/base.css delete mode 100644 assets/css/comparison.css delete mode 100644 assets/css/export.css delete mode 100644 assets/css/floating-elements.css delete mode 100644 assets/css/folder-metadata.css delete mode 100644 assets/css/folder-tree.css delete mode 100644 assets/css/grid.css delete mode 100644 assets/css/main.css delete mode 100644 assets/css/metadata-section.css delete mode 100644 assets/css/metadata.css delete mode 100644 assets/css/navigation.css delete mode 100644 assets/css/responsive.css delete mode 100644 assets/css/search.css delete mode 100644 assets/css/sidebar.css delete mode 100644 assets/css/sort-controls.css delete mode 100644 assets/css/sort-debug.css delete mode 100644 assets/css/stats.css delete mode 100644 assets/css/variables.css delete mode 100644 assets/css/view-controls.css delete mode 100644 assets/css/view-override.css delete mode 100644 assets/js/comparison-manager.js delete mode 100644 assets/js/export-manager.js delete mode 100644 assets/js/folder-metadata.js delete mode 100644 assets/js/gallery-app.js delete mode 100644 assets/js/keyboard-manager.js delete mode 100644 assets/js/main.js delete mode 100644 assets/js/metadata-popup.js delete mode 100644 assets/js/metadata-section.js delete mode 100644 assets/js/navigation-manager.js delete mode 100644 assets/js/recent-plots-manager.js delete mode 100644 assets/js/search-manager.js delete mode 100644 assets/js/sort-manager.js delete mode 100644 assets/js/stats-manager.js delete mode 100644 assets/js/theme-manager.js delete mode 100644 assets/js/utils.js delete mode 100644 assets/js/view-manager.js delete mode 100644 docs/COVERAGE_TESTING.md delete mode 100644 docs/SORT_IMPLEMENTATION.md delete mode 100644 gallery/assets/css/export.css delete mode 100644 gallery/assets/css/sort-debug.css delete mode 100644 gallery/assets/js/export-manager.js delete mode 100644 gallery/utils/backup.py delete mode 100644 generate_gallery.py delete mode 100644 orchestration/backup.py delete mode 100644 orchestration/config.py delete mode 100644 orchestration/logger.py delete mode 100644 orchestration/metadata.py delete mode 100644 orchestration/pdf_export.py delete mode 100644 python/add_creation_time.py delete mode 100644 python/add_metadata.py delete mode 100644 templates/gallery.html delete mode 100644 test.ipynb delete mode 100644 test_logger.py delete mode 100644 tests/test_backup.py diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 47b059e..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "flake8.args": [ - "--max-line-length=120", - "--ignore=W293,E123,W503", - ], -} \ No newline at end of file diff --git a/README.md b/README.md index 3203292..1d7c312 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,12 @@ ## Installation -### From GitLab +### From Gitea Pip install: ```bash -pip install git+https://gitlab.etp.kit.edu/kschmidt/web +pip install git+https://git.larsbogner.de/lars/ETPlot ``` Or git clone and `pip install .`. After installation the `gallery` command is available in your shell. Verify with: @@ -184,7 +184,6 @@ paths: gallery: plot_root: "gallery" # subdirectory inside web_folder png_dpi: 400 # thumbnail resolution - backup_folder: "" # optional backup path sources: - name: "analysis_results" diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc deleted file mode 100644 index 9d7e9207ae3eb0fd9a86e3e13268665cfa31f846..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5718 zcmb_gU2Gf25#IaZ-;oq4>c_T}#HUF9q3hC4kTj`aq;>4dQDG}?oD_+G_IkQoQfD83 zx_7ipfocud0TduJdS|)W=Q+IX4)*T1xdc{=hu4P-U1+8M0sbzfKs+3ftyH}-JRNYysIvZav z1z&d>j!VmjRjXpybravL>F%s^SPd1Z>jK+3th_V{qo75~XvLtQfnUebs7|%cfKyQZ zgZAeoFA)yXixe$tn_g6ROv`Zdf+wGa{-+Go>=Y@e+#QF?5}{;TfPfKSf{>V&s7xcK zcnl=GCWSRQtVOWah~{ISf}Q&gojc__!gsvTV>$Bf)}5wvJ0ANF;AJJ zGg`4~n-uCs$l{aFVJGT=s zC0e0DF!KnpoSLmSR40g|f|sb-Raaxxs_VsSm6B%GEH6Ey8yD)e!0A)@7z0Eh4DDqZ zBv~ZANOC|t$*Q=l51ZWF0j%xn5Ue=JLNA?cWpY=hTIuYS)2+V2Tk@d)2I}nWp#30u zfP*j`xr1OuN4)q;llS*X-JuK?mP=VtH>f&8XZ4E~gdw`6Yb6!DY+1jgK_tSe*U&kR zmvgAjj9Ja3Gu1jQH?8bI!0qUiVHhuW%2t0MN--pR;qRdDe18n?S(o>;b`Rf@hkak5 z&KL4U7%bcu$sj}~0Z`_TzCgi52P^`ZQXN1L0I@E#;An-I6ogH|lX1a#vcI8o<6 z*%$D8fSwq$MB}d&70Mais%*{#Do-7d40Ko3OC?52x{Ha#wmTECu~2yz zoSf|>A6%t#Iy0fuGE9^;gSn@H$*D#ePeK^mW$MmWO+O*oRUNYm@5boPNotW*%jzq~ z&zx3^mW|m1N9;v#{v9tCgoP*8El3&}lNR;5?P{=)0InNeEXX-VkaWT0gkW8Q|J?$R zCDJ74;kmV(54IHD{BtBP%s=t$1C49&hy`4RCng4-MGAS5VYD&a3B3);&7pSD!EvaJ zV-oV`d;Li%Y-ANobPgidWzvq4Z2z0{ug%}ijICwHR^wwlOut0Kp87n7ot-*`k#dnT z2e%<7&rYIk2VjT{!S)Dv}a!14Vt!4JD#`k^MhJCK-!i89`Z4E^}UAR5i zHeJY^?+@(qUk-BZ&WQqf1rSmoumAFr^a`014B!~zLUNAeQ~GIeJqKU};nRTGThayx zYkdQ*3reVcoRysMu!Zl3;WDnC1vFRPDnEU2tOtzgI_+RgZS?d4kF718`7sOID96=d zBc#q*&?9CQYKEz*s|($NgVE;UOu$E`s_q1Aq|9$IR%PSrq<qY$FX8A#s-gLwRHCkbGT<9uzS|qY-f?e~PkWMKJM1y2dqUBZaqHzNJh^JTo(eLo z--^|(fE^@O-r6!MFX{`qB@Tu)&URRM#`6*wDjE(p#Luf4onT4C!0hZdFAuHmIi*DkFj zu0OuMtFV}Ae% z+Yyq;wh5@-QAoCLdEk2NooC;V-O4<-8h`HID2#eFJ@VgAc7p=nZ@)2kA|d^5_vncM z`S*#)iCk=I6L8{0ao}8p$`=PtH-yG!2oOzx&zsVX0C3@QMaEwTH1I-VD=ai*9~SZv zxTfgL={fx(8gSPFNWp=oW0fb&O~9ry7j7!;>YV1d4S6+#F+3jR zCH7Th<8oqr^M;;<{`{Daxm7>nI~$^)T>~Wt58M#%{hQ)Fm3?#awaK3qmtXv;y>k4$ zeIKO0$)o;udSop<@|lDA<&8(5mfnjT7h?H=9S?r|Rsc`K{>hgE0HVGp+W=xd+#n!$ z@nC=6;M-JxKgV}JxGR854ZKbA_w4=&`#APFfn>`Gdmr+UpfjA`0C|<%kt30KJ4I51 z>+(P=J+v+lwc>Et{%~afJMOKKW0y}YzWCNx*3yrx%g6o^N#7C0$iBN0iT1Ry){a9B zd1E1OEadGEdHX}&p^$e7dA)6MZ)8$f8Eq4MUO)H=f1*+#YCkI^!HFwzN00FOv$u@6hxj+_nwF!_wfXyB*kc5W- z=7fg=0X9>CAcKNTD98kYGlFnjXcPVjgg)yBB9T*z^M981w983~Vnl8L diff --git a/assets/css/base.css b/assets/css/base.css deleted file mode 100644 index 2da21fc..0000000 --- a/assets/css/base.css +++ /dev/null @@ -1,71 +0,0 @@ -/* ======================================== - BASE LAYOUT AND TYPOGRAPHY - ======================================== */ -* { - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - margin: 0; - padding: 1rem; - padding-bottom: 2rem; /* Reduced from 400px - stats box is fixed positioned */ - background-color: var(--bg-color); - color: var(--text-color); - transition: background-color 0.3s, color 0.3s; - line-height: 1.5; -} - -h1 { - font-size: 1.8rem; - margin-bottom: 0.5rem; - font-weight: 600; -} - -h2 { - color: var(--text-color); - font-size: 1.3rem; - margin: 1.5rem 0 0.5rem 0; -} - -/* ======================================== - SUBDIRECTORIES LIST - ======================================== */ -ul { - list-style: none; - padding: 0; -} - -ul li { - margin: 0.5rem 0; -} - -ul li a { - color: var(--link-color); - text-decoration: none; - padding: 0.3rem 0; - display: inline-block; - transition: color 0.2s ease; -} - -ul li a:hover { - color: var(--link-hover); - text-decoration: underline; -} - -/* Responsive design adjustments */ -@media (max-width: 768px) { - body { - padding: 0.5rem; - padding-bottom: 300px; /* Further increased mobile bottom padding to match desktop */ - } - - h1 { - font-size: 1.5rem; - } - - h2 { - font-size: 1.2rem; - margin: 1rem 0 0.5rem 0; - } -} diff --git a/assets/css/comparison.css b/assets/css/comparison.css deleted file mode 100644 index 7e2dee2..0000000 --- a/assets/css/comparison.css +++ /dev/null @@ -1,180 +0,0 @@ -/* ======================================== - PLOT COMPARISON OVERLAY - ======================================== */ -.comparison-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.9); - z-index: 2000; - display: none; - backdrop-filter: blur(4px); -} - -.comparison-overlay.open { - display: flex; - align-items: center; - justify-content: center; -} - -.comparison-container { - width: 95%; - height: 90%; - background: var(--bg-color); - border-radius: 12px; - padding: 1.5rem; - display: flex; - flex-direction: column; - position: relative; - box-shadow: 0 10px 30px rgba(0,0,0,0.5); -} - -.comparison-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; - padding-bottom: 1rem; - border-bottom: 1px solid var(--border-color); -} - -.comparison-title { - font-size: 1.5rem; - font-weight: 600; - color: var(--text-color); - margin: 0; -} - -.comparison-close { - background: var(--button-bg); - color: white; - border: none; - border-radius: 50%; - width: 40px; - height: 40px; - font-size: 1.2rem; - cursor: pointer; - transition: all 0.2s ease; -} - -.comparison-close:hover { - background: var(--button-hover); - transform: scale(1.1); -} - -.comparison-content { - flex: 1; - display: flex; - gap: 1rem; - overflow: hidden; -} - -.comparison-panel { - flex: 1; - display: flex; - flex-direction: column; - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - overflow: hidden; -} - -.comparison-panel-header { - background: var(--tree-bg); - padding: 0.8rem 1rem; - border-bottom: 1px solid var(--border-color); - display: flex; - justify-content: space-between; - align-items: center; -} - -.comparison-panel-title { - font-weight: 600; - color: var(--text-color); - font-size: 1rem; -} - -.comparison-replace-btn { - background: var(--success-color); - color: white; - border: none; - padding: 0.4rem 0.8rem; - border-radius: 4px; - font-size: 0.85rem; - cursor: pointer; - transition: all 0.2s ease; -} - -.comparison-replace-btn:hover { - background: var(--success-hover); -} - -.comparison-panel-content { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - padding: 1rem; - overflow: auto; -} - -.comparison-plot-container { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - position: relative; -} - -.comparison-plot { - max-width: 100%; - max-height: 100%; - border: 1px solid var(--border-color); - border-radius: 4px; - cursor: pointer; - transition: transform 0.2s ease; -} - -.comparison-plot:hover { - transform: scale(1.02); -} - -.comparison-placeholder { - width: 100%; - height: 300px; - border: 2px dashed var(--border-color); - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - color: var(--breadcrumb-color); - font-size: 1.1rem; - cursor: pointer; - transition: all 0.2s ease; -} - -.comparison-placeholder:hover { - border-color: var(--button-bg); - color: var(--button-bg); -} - -.comparison-plot-info { - position: absolute; - bottom: 0.5rem; - left: 0.5rem; - right: 0.5rem; - background: rgba(0, 0, 0, 0.8); - color: white; - padding: 0.5rem; - border-radius: 4px; - font-size: 0.9rem; - opacity: 0; - transition: opacity 0.2s ease; -} - -.comparison-plot-container:hover .comparison-plot-info { - opacity: 1; -} diff --git a/assets/css/export.css b/assets/css/export.css deleted file mode 100644 index 84d7b5f..0000000 --- a/assets/css/export.css +++ /dev/null @@ -1,440 +0,0 @@ -/* ======================================== - EXPORT FUNCTIONALITY STYLES - ======================================== */ - -/* Selection mode styles */ -.selection-mode .grid-item { - cursor: pointer; - transition: all 0.2s ease; -} - -.selection-mode .grid-item:hover { - transform: scale(1.02); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); -} - -/* Selection overlay */ -.selection-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.3); - display: none; - justify-content: center; - align-items: center; - border-radius: 8px; - z-index: 5; -} - -.selection-mode .selection-overlay { - display: flex; -} - -.selection-checkbox { - background: var(--card-background); - border: 2px solid var(--border-color); - border-radius: 50%; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - font-size: 18px; - transition: all 0.2s ease; -} - -.grid-item.selected .selection-checkbox { - background: var(--primary-color, #007bff); - border-color: var(--primary-color, #007bff); - color: white; -} - -.checkbox-icon { - line-height: 1; -} - -/* Selection counter */ -.selection-counter { - position: fixed; - bottom: 100px; - right: 20px; - background: var(--card-background); - border: 1px solid var(--border-color); - border-radius: 20px; - padding: 8px 16px; - font-size: 14px; - font-weight: 500; - color: var(--text-color); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - z-index: 999; -} - -/* Export button styles */ -.export-btn { - background: #28a745 !important; -} - -.export-btn:hover { - background: #218838 !important; -} - -.export-btn:disabled { - background: #6c757d !important; - cursor: not-allowed; -} - -/* Export messages */ -.export-message { - position: fixed; - top: 20px; - right: 20px; - padding: 12px 24px; - border-radius: 6px; - font-weight: 500; - z-index: 1001; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - animation: slideIn 0.3s ease; -} - -.export-message-info { - background: #d1ecf1; - color: #0c5460; - border: 1px solid #bee5eb; -} - -.export-message-success { - background: #d4edda; - color: #155724; - border: 1px solid #c3e6cb; -} - -.export-message-warning { - background: #fff3cd; - color: #856404; - border: 1px solid #ffeaa7; -} - -.export-message-error { - background: #f8d7da; - color: #721c24; - border: 1px solid #f5c6cb; -} - -@keyframes slideIn { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -/* Selection mode indicator */ -.selection-mode::before { - content: "Selection Mode - Click plots to select them"; - position: fixed; - top: 0; - left: 0; - right: 0; - background: var(--primary-color, #007bff); - color: white; - text-align: center; - padding: 8px; - font-size: 14px; - font-weight: 500; - z-index: 1000; -} - -/* Adjust main content when in selection mode */ -.selection-mode { - padding-top: 40px; -} - -/* Export instructions overlay */ -.export-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1002; -} - -.export-instructions { - background: var(--card-background); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 24px; - max-width: 600px; - max-height: 80vh; - overflow-y: auto; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); -} - -.export-instructions h3 { - margin: 0 0 16px 0; - color: var(--text-color); -} - -.export-instructions p { - margin: 0 0 16px 0; - color: var(--text-color); -} - -.export-data { - margin: 16px 0; -} - -.export-data textarea { - width: 100%; - height: 200px; - font-family: 'Courier New', monospace; - font-size: 12px; - border: 1px solid var(--border-color); - border-radius: 4px; - padding: 8px; - background: var(--background-color); - color: var(--text-color); - resize: vertical; -} - -.export-commands { - margin: 16px 0; - padding: 12px; - background: var(--header-background); - border-radius: 4px; - border: 1px solid var(--border-color); -} - -.export-command-container { - margin: 20px 0; - border: 1px solid var(--border-color); - border-radius: 8px; - overflow: hidden; -} - -.export-command { - background: var(--header-background); - padding: 16px; - border-bottom: 1px solid var(--border-color); -} - -.export-command code { - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 13px; - line-height: 1.4; - color: var(--text-color); - word-break: break-all; - display: block; - background: none; - border: none; - padding: 0; - margin: 0; -} - -.export-actions { - display: flex; - gap: 8px; - padding: 12px 16px; - background: var(--card-background); -} - -.export-actions button { - padding: 8px 16px; - border: 1px solid var(--border-color); - border-radius: 4px; - background: var(--card-background); - color: var(--text-color); - cursor: pointer; - transition: all 0.2s ease; -} - -.export-actions button:hover { - background: var(--header-background); -} - -.export-actions button:last-child { - background: var(--primary-color, #007bff); - color: white; - border-color: var(--primary-color, #007bff); -} - -.export-actions button:last-child:hover { - background: var(--primary-color-dark, #0056b3); -} - -.copy-btn, .close-btn { - padding: 8px 16px; - border: 1px solid var(--border-color); - border-radius: 6px; - background: var(--card-background); - color: var(--text-color); - cursor: pointer; - transition: all 0.2s ease; - font-size: 14px; - display: flex; - align-items: center; - gap: 4px; -} - -.copy-btn:hover { - background: var(--primary-color, #007bff); - color: white; - border-color: var(--primary-color, #007bff); -} - -.close-btn { - background: #dc3545; - color: white; - border-color: #dc3545; - margin-left: auto; -} - -.close-btn:hover { - background: #c82333; - border-color: #bd2130; -} - -.export-details, .export-tips { - margin: 20px 0; - padding: 16px; - border-radius: 6px; - border: 1px solid var(--border-color); -} - -.export-details { - background: var(--header-background); -} - -.export-tips { - background: var(--card-background); - border-color: var(--primary-color, #007bff); - border-left: 4px solid var(--primary-color, #007bff); -} - -.export-details h4, .export-tips h4 { - margin: 0 0 12px 0; - color: var(--text-color); - font-size: 16px; -} - -.export-details ul, .export-tips ul { - margin: 0; - padding-left: 20px; - color: var(--text-color); -} - -.export-details li, .export-tips li { - margin: 8px 0; - line-height: 1.5; -} - -.export-details code, .export-tips code { - background: var(--background-color); - padding: 2px 6px; - border-radius: 3px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 12px; - border: 1px solid var(--border-color); -} - -.export-tips kbd { - background: var(--header-background); - border: 1px solid var(--border-color); - border-radius: 3px; - padding: 2px 6px; - font-family: inherit; - font-size: 12px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); -} - -.copy-feedback { - position: absolute; - top: 10px; - right: 10px; - padding: 8px 12px; - border-radius: 4px; - font-size: 12px; - font-weight: 500; - z-index: 1003; - animation: fadeInOut 2s ease-in-out; -} - -.copy-feedback-success { - background: #d4edda; - color: #155724; - border: 1px solid #c3e6cb; -} - -.copy-feedback-error { - background: #f8d7da; - color: #721c24; - border: 1px solid #f5c6cb; -} - -@keyframes fadeInOut { - 0% { opacity: 0; transform: translateY(-10px); } - 20% { opacity: 1; transform: translateY(0); } - 80% { opacity: 1; transform: translateY(0); } - 100% { opacity: 0; transform: translateY(-10px); } -} - -/* Dark theme adjustments */ -[data-theme="dark"] .selection-checkbox { - background: var(--card-background); - border-color: var(--border-color); -} - -[data-theme="dark"] .grid-item.selected .selection-checkbox { - background: var(--primary-color, #0d6efd); - border-color: var(--primary-color, #0d6efd); -} - -[data-theme="dark"] .export-message-info { - background: #0c5460; - color: #d1ecf1; - border-color: #086972; -} - -[data-theme="dark"] .export-message-success { - background: #155724; - color: #d4edda; - border-color: #1e7e34; -} - -[data-theme="dark"] .export-message-warning { - background: #856404; - color: #fff3cd; - border-color: #b58b14; -} - -[data-theme="dark"] .export-message-error { - background: #721c24; - color: #f8d7da; - border-color: #a94442; -} - -[data-theme="dark"] .copy-feedback-success { - background: #155724; - color: #d4edda; - border-color: #1e7e34; -} - -[data-theme="dark"] .copy-feedback-error { - background: #721c24; - color: #f8d7da; - border-color: #a94442; -} - -[data-theme="dark"] .export-tips kbd { - background: var(--background-color); - color: var(--text-color); -} diff --git a/assets/css/floating-elements.css b/assets/css/floating-elements.css deleted file mode 100644 index cd8e830..0000000 --- a/assets/css/floating-elements.css +++ /dev/null @@ -1,112 +0,0 @@ -/* ======================================== - FLOATING ACTION BUTTONS - ======================================== */ -.floating-buttons { - position: fixed; - bottom: 80px; - right: 15px; - display: flex; - flex-direction: column; - gap: 10px; - z-index: 1000; - /* Ensure buttons don't interfere with content */ - pointer-events: none; -} - -.floating-btn { - width: 56px; - height: 56px; - border-radius: 50%; - border: none; - cursor: pointer; - font-size: 1.3rem; - box-shadow: 0 3px 10px rgba(0,0,0,0.3); - transition: all 0.2s ease; - display: flex; - align-items: center; - justify-content: center; - /* Re-enable pointer events for buttons */ - pointer-events: auto; -} - -.floating-btn:hover { - transform: scale(1.1); -} - -.sidebar-toggle { - background: var(--button-bg); - color: white; -} - -.theme-toggle { - background: var(--button-bg); - color: white; -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .floating-buttons { - bottom: 60px; - right: 10px; - gap: 8px; - } - - .floating-btn { - width: 48px; - height: 48px; - font-size: 1.1rem; - } -} - -/* ======================================== - KEYBOARD SHORTCUTS HELP - ======================================== */ -.shortcuts-help { - position: fixed; - bottom: 220px; - right: 15px; - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 1rem; - display: none; - box-shadow: 0 4px 15px rgba(0,0,0,0.2); - z-index: 1001; - font-size: 0.9rem; - max-width: 280px; - max-height: 400px; - overflow-y: auto; -} - -.shortcuts-help h4 { - margin: 0 0 0.8rem 0; - color: var(--text-color); - font-size: 1rem; -} - -.shortcut-item { - display: flex; - justify-content: space-between; - align-items: center; - margin: 0.4rem 0; -} - -.shortcut-key { - background: var(--border-color); - padding: 0.2rem 0.4rem; - border-radius: 4px; - font-family: 'JetBrains Mono', 'Courier New', monospace; - font-size: 0.8rem; - font-weight: 500; -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .shortcuts-help { - bottom: 180px; - right: 10px; - left: 10px; - max-width: none; - max-height: 300px; - } -} diff --git a/assets/css/folder-metadata.css b/assets/css/folder-metadata.css deleted file mode 100644 index 8508cf3..0000000 --- a/assets/css/folder-metadata.css +++ /dev/null @@ -1,179 +0,0 @@ -/* ======================================== - FOLDER METADATA STYLES - ======================================== */ - -/* Folder Metadata Container */ -.folder-metadata-container { - margin: 1rem 0; - border-radius: 8px; - background: var(--card-bg); - border: 1px solid var(--border-color); - overflow: hidden; -} - -/* Folder Metadata Toggle Button */ -.folder-metadata-toggle { - width: 100%; - padding: 0.75rem 1rem; - background: var(--card-bg); - border: none; - cursor: pointer; - display: flex; - align-items: center; - justify-content: space-between; - transition: background-color 0.2s ease; - font-size: 0.95rem; - color: var(--text-color); - position: relative; - z-index: 10; - outline: none; -} - -.folder-metadata-toggle:hover { - background: var(--button-hover); - color: white; -} - -.folder-metadata-toggle:focus { - outline: 2px solid var(--link-color); - outline-offset: 2px; -} - -.folder-metadata-icon { - margin-right: 0.5rem; -} - -.folder-metadata-label { - flex: 1; - text-align: left; - font-weight: 500; -} - -.folder-metadata-arrow { - transition: transform 0.2s ease; - font-size: 0.8rem; -} - -.folder-metadata-container.expanded .folder-metadata-arrow { - transform: rotate(180deg); -} - -/* Folder Metadata Content - HIDDEN BY DEFAULT */ -.folder-metadata-content { - max-height: 0; - overflow: hidden; - transition: max-height 0.3s ease, opacity 0.3s ease; - background: var(--bg-color); - opacity: 0; - display: none; /* Force hide initially */ -} - -/* Show content when expanded */ -.folder-metadata-container.expanded .folder-metadata-content { - max-height: 1000px; - border-top: 1px solid var(--border-color); - opacity: 1; - display: block; /* Show when expanded */ -} - -/* Folder Metadata Grid */ -.folder-metadata-grid { - padding: 1rem; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 0.5rem 1rem; -} - -/* Folder Metadata Items */ -.folder-metadata-item { - display: flex; - align-items: flex-start; - gap: 0.5rem; - padding: 0.25rem 0; -} - -.folder-metadata-key { - font-weight: 600; - color: var(--link-color); - white-space: nowrap; - min-width: fit-content; -} - -.folder-metadata-value { - color: var(--text-color); - word-break: break-word; - flex: 1; -} - -/* Tags for list items */ -.folder-metadata-list { - display: flex; - flex-wrap: wrap; - gap: 0.25rem; -} - -.folder-metadata-tag { - background: var(--link-color); - color: white; - padding: 0.2rem 0.5rem; - border-radius: 12px; - font-size: 0.8rem; - white-space: nowrap; -} - -/* Nested metadata */ -.folder-metadata-nested { - background: var(--card-bg); - padding: 0.5rem; - border-radius: 4px; - border-left: 3px solid var(--link-color); -} - -.folder-metadata-nested-item { - margin: 0.25rem 0; - font-size: 0.9rem; -} - -/* Long text handling */ -.folder-metadata-expand { - background: none; - border: none; - color: var(--link-color); - cursor: pointer; - text-decoration: underline; - padding: 0; - margin-left: 0.5rem; - font-size: 0.85rem; -} - -.folder-metadata-expand:hover { - color: var(--link-hover); -} - -/* Links */ -.folder-metadata-value a { - color: var(--link-color); - text-decoration: none; -} - -.folder-metadata-value a:hover { - color: var(--link-hover); - text-decoration: underline; -} - -/* Responsive design */ -@media (max-width: 768px) { - .folder-metadata-grid { - grid-template-columns: 1fr; - gap: 0.5rem; - } - - .folder-metadata-item { - flex-direction: column; - gap: 0.25rem; - } - - .folder-metadata-key { - white-space: normal; - } -} diff --git a/assets/css/folder-tree.css b/assets/css/folder-tree.css deleted file mode 100644 index 3c394ad..0000000 --- a/assets/css/folder-tree.css +++ /dev/null @@ -1,40 +0,0 @@ -/* ======================================== - FOLDER TREE - ======================================== */ -.folder-tree { - background: var(--tree-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 1rem; - margin: 1rem 0; - font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace; - font-size: 0.85rem; - max-height: 350px; - overflow-y: auto; - transition: all 0.3s ease; -} - -.tree-item { - margin: 0.2rem 0; - white-space: pre; - font-family: inherit; -} - -.tree-current { - background: var(--tree-current-bg); - color: white; - padding: 0.2rem 0.4rem; - border-radius: 4px; - font-weight: 500; -} - -.tree-link { - color: var(--link-color); - text-decoration: none; - transition: color 0.2s ease; -} - -.tree-link:hover { - text-decoration: underline; - color: var(--link-hover); -} diff --git a/assets/css/grid.css b/assets/css/grid.css deleted file mode 100644 index 8ad9828..0000000 --- a/assets/css/grid.css +++ /dev/null @@ -1,106 +0,0 @@ -/* ======================================== - PLOT GRID - ======================================== */ -.grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 0.8rem; - padding: 1rem 0; -} - -.grid-item { - text-align: center; - background: var(--card-bg); - border-radius: 8px; - padding: 0.8rem; - transition: all 0.2s ease; - border: 1px solid transparent; -} - -.grid-item:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0,0,0,0.1); - border-color: var(--border-color); -} - -.grid-item img { - max-width: 100%; - border: 1px solid var(--border-color); - border-radius: 6px; - transition: all 0.2s ease; -} - -.grid-item a { - color: var(--link-color); - text-decoration: none; -} - -.grid-item.highlighted { - border-color: var(--button-bg); - box-shadow: 0 0 15px rgba(0, 120, 212, 0.3); - transform: translateY(-2px); - animation: highlightPulse 2s ease-in-out; -} - -@keyframes highlightPulse { - 0%, 100% { transform: translateY(-2px) scale(1); } - 50% { transform: translateY(-2px) scale(1.02); } -} - -.plot-name { - margin-top: 0.8rem; - word-wrap: break-word; - word-break: break-word; - hyphens: auto; - font-size: 0.9rem; - line-height: 1.3; - max-height: 3.9rem; - overflow: hidden; - padding: 0 0.2rem; - font-weight: 500; -} - -/* Plot selection mode */ -.selecting-plots .grid-item { - cursor: pointer !important; - transition: all 0.2s ease; - position: relative; -} - -.selecting-plots .grid-item:hover { - transform: translateY(-4px); - box-shadow: 0 6px 20px rgba(0, 120, 212, 0.3); - border-color: var(--button-bg); -} - -.selecting-plots .grid-item::before { - content: '📊 Click to compare'; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: rgba(0, 120, 212, 0.95); - color: white; - padding: 0.5rem 1rem; - border-radius: 4px; - font-size: 0.9rem; - font-weight: 600; - opacity: 0; - transition: opacity 0.2s ease; - pointer-events: none; - z-index: 10; - white-space: nowrap; -} - -.selecting-plots .grid-item:hover::before { - opacity: 1; -} - -/* Ensure grid items are clickable in selection mode */ -.selecting-plots .grid-item * { - pointer-events: none; -} - -.selecting-plots .grid-item { - pointer-events: auto; -} diff --git a/assets/css/main.css b/assets/css/main.css deleted file mode 100644 index c6019d7..0000000 --- a/assets/css/main.css +++ /dev/null @@ -1,33 +0,0 @@ -/* ======================================== - GALLERY STYLES - MAIN ENTRY POINT - ======================================== */ - -/* Core styles */ -@import url('./variables.css'); -@import url('./base.css'); - -/* Component styles */ -@import url('./navigation.css'); -@import url('./search.css'); -@import url('./folder-tree.css'); -@import url('./grid.css'); -@import url('./sidebar.css'); -@import url('./floating-elements.css'); -@import url('./stats.css'); -@import url('./comparison.css'); -@import url('./metadata.css'); -@import url('./metadata-section.css'); -@import url('./folder-metadata.css'); -@import url('./export.css'); - -/* View controls - must come after grid.css to override */ -@import url('./view-controls.css'); - -/* Sort controls styling */ -@import url('./sort-controls.css'); - -/* View override - force grid layout to work */ -@import url('./view-override.css'); - -/* Responsive design - last to override everything */ -@import url('./responsive.css'); diff --git a/assets/css/metadata-section.css b/assets/css/metadata-section.css deleted file mode 100644 index 7988467..0000000 --- a/assets/css/metadata-section.css +++ /dev/null @@ -1,383 +0,0 @@ -/* ======================================== - METADATA SECTION STYLES - ======================================== */ - -/* Metadata Section Container */ -.metadata-section { - margin: 1rem 0; - border-radius: 8px; - background: var(--card-bg); - border: 1px solid var(--border-color); - overflow: hidden; -} - -/* Toggle Button */ -.metadata-toggle-btn { - width: 100%; - padding: 0.75rem 1rem; - background: var(--card-bg); - border: none; - cursor: pointer; - display: flex; - align-items: center; - justify-content: space-between; - transition: background-color 0.2s ease; - font-size: 0.95rem; - color: var(--text-color); - position: relative; - z-index: 10; -} - -.metadata-toggle-btn:hover { - background: var(--button-hover); - color: white; -} - -.metadata-toggle-btn:focus { - outline: 2px solid var(--link-color); - outline-offset: 2px; -} - -.metadata-icon { - margin-right: 0.5rem; -} - -.metadata-label { - flex: 1; - text-align: left; - font-weight: 500; -} - -.metadata-arrow { - transition: transform 0.2s ease; - font-size: 0.8rem; -} - -/* Content Area - Hidden by default */ -.metadata-content { - background: var(--bg-color); - border-top: 1px solid var(--border-color); - display: none; /* Hidden by default */ -} - -/* Metadata Header with File Path */ -.metadata-header { - padding: 1rem; - background: var(--card-bg); - border-bottom: 1px solid var(--border-color); -} - -.metadata-file-info { - display: flex; - align-items: center; - gap: 0.5rem; - flex-wrap: wrap; -} - -.file-path-label { - font-weight: 600; - color: var(--text-color); - white-space: nowrap; - font-size: 0.9rem; -} - -.file-path { - background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: 4px; - padding: 0.4rem 0.6rem; - font-family: 'Courier New', monospace; - font-size: 0.8rem; - color: var(--link-color); - flex: 1; - min-width: 200px; - word-break: break-all; - user-select: all; -} - -.copy-path-btn { - background: var(--link-color); - color: white; - border: none; - border-radius: 4px; - padding: 0.4rem 0.8rem; - cursor: pointer; - font-size: 0.8rem; - transition: all 0.2s ease; - white-space: nowrap; - font-weight: 500; -} - -.copy-path-btn:hover { - background: var(--link-hover); - transform: translateY(-1px); -} - -.copy-path-btn:active { - transform: scale(0.95); -} - -.copy-path-btn.copied { - background: #4CAF50; - transform: scale(1.05); -} - -/* Tip Icon with Hover Tooltip */ -.tip-icon { - cursor: help; - font-size: 1.2rem; - position: relative; - display: inline-block; - margin-left: 0.25rem; - opacity: 0.8; - transition: opacity 0.2s ease; -} - -.tip-icon:hover { - opacity: 1; -} - -/* Custom tooltip for tip icon */ -.tip-icon::after { - content: attr(title); - position: absolute; - bottom: 125%; - left: 50%; - transform: translateX(-50%); - background: #333; - color: white; - padding: 0.75rem; - border-radius: 6px; - font-size: 0.85rem; - white-space: normal; - width: 280px; - text-align: left; - z-index: 1000; - opacity: 0; - visibility: hidden; - transition: opacity 0.3s ease, visibility 0.3s ease; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - line-height: 1.4; - font-weight: normal; - font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif); -} - -/* Tooltip arrow */ -.tip-icon::before { - content: ''; - position: absolute; - bottom: 115%; - left: 50%; - transform: translateX(-50%); - border: 6px solid transparent; - border-top-color: #333; - opacity: 0; - visibility: hidden; - transition: opacity 0.3s ease, visibility 0.3s ease; - z-index: 1001; -} - -.tip-icon:hover::after, -.tip-icon:hover::before { - opacity: 1; - visibility: visible; -} - -/* Grid Layout */ -.metadata-grid { - padding: 1rem; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 1rem 1.5rem; /* Increased gaps to prevent overlapping */ -} - -/* Metadata Items */ -.metadata-item { - display: flex; - flex-direction: column; /* Stack key and value vertically to prevent overlap */ - gap: 0.25rem; - padding: 0.75rem; - background: var(--card-bg); - border-radius: 6px; - border: 1px solid var(--border-color); - word-wrap: break-word; /* Ensure long text wraps */ - overflow-wrap: break-word; /* Additional word wrapping */ -} - -.metadata-key { - font-weight: 600; - color: var(--link-color); - font-size: 0.9rem; - margin-bottom: 0.25rem; -} - -.metadata-value { - color: var(--text-color); - word-break: break-word; - overflow-wrap: break-word; - line-height: 1.4; - font-size: 0.9rem; -} - -/* LaTeX content styling */ -.latex-content { - color: var(--text-color); - line-height: 1.6; - font-family: 'Times New Roman', serif; -} - -/* Simple list items - proper list formatting */ -.metadata-list { - color: var(--text-color); - line-height: 1.4; - margin: 0; - padding-left: 1.2rem; - list-style-type: disc; /* Add bullet points */ -} - -.metadata-list li { - margin: 0.25rem 0; - padding: 0; - color: var(--text-color); -} - -/* YAML-style formatting for metadata */ -.metadata-yaml-list { - margin: 0.5rem 0; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 0.9rem; - line-height: 1.6; - background: var(--card-bg); - padding: 0.8rem; - border-radius: 6px; - border: 1px solid var(--border-color); -} - -.yaml-list-item { - color: var(--text-color); - margin: 0.25rem 0; - padding-left: 0; - text-indent: 0; -} - -.metadata-yaml-object { - margin: 0.5rem 0; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 0.9rem; - line-height: 1.6; - background: var(--card-bg); - padding: 0.8rem; - border-radius: 6px; - border: 1px solid var(--border-color); -} - -.yaml-object-item { - margin: 0.5rem 0; -} - -.yaml-key { - color: var(--link-color); - font-weight: 600; -} - -.yaml-value { - color: var(--text-color); - margin-left: 0.5rem; -} - -.yaml-nested-list { - margin: 0.25rem 0 0 1.5rem; - border-left: 2px solid var(--border-color); - padding-left: 0.8rem; -} - -.yaml-nested-item { - color: var(--text-color); - margin: 0.2rem 0; - padding-left: 0; -} - -.yaml-nested-object { - margin: 0.25rem 0 0 1.5rem; - border-left: 2px solid var(--border-color); - padding-left: 0.8rem; -} - -/* Remove the blue box styling for tags */ -.metadata-tag { - display: inline; - background: none; - color: var(--text-color); - padding: 0; - border-radius: 0; - font-size: inherit; - white-space: normal; -} - -/* Simplified nested metadata */ -.metadata-nested { - background: none; - padding: 0; - border-radius: 0; - border-left: none; - color: var(--text-color); -} - -.metadata-nested-item { - margin: 0.25rem 0; - font-size: 0.9rem; -} - -/* Long text handling */ -.metadata-expand { - background: none; - border: none; - color: var(--link-color); - cursor: pointer; - text-decoration: underline; - padding: 0; - margin-left: 0.5rem; - font-size: 0.85rem; -} - -.metadata-expand:hover { - color: var(--link-hover); -} - -/* Links */ -.metadata-value a { - color: var(--link-color); - text-decoration: none; -} - -.metadata-value a:hover { - color: var(--link-hover); - text-decoration: underline; -} - -/* Responsive design */ -@media (max-width: 768px) { - .metadata-grid { - grid-template-columns: 1fr; - gap: 0.5rem; - } - - .metadata-item { - flex-direction: column; - gap: 0.25rem; - } - - .metadata-key { - white-space: normal; - } - - .metadata-file-info { - flex-direction: column; - align-items: stretch; - gap: 0.5rem; - } - - .file-path { - min-width: auto; - } -} diff --git a/assets/css/metadata.css b/assets/css/metadata.css deleted file mode 100644 index 872fef6..0000000 --- a/assets/css/metadata.css +++ /dev/null @@ -1,180 +0,0 @@ -/* ======================================== - METADATA POPUP STYLES - ======================================== */ - -/* Metadata button on thumbnails */ -.metadata-btn { - position: absolute; - top: 8px; - right: 8px; - background: rgba(0, 0, 0, 0.7); - color: white; - border: none; - border-radius: 50%; - width: 32px; - height: 32px; - font-size: 16px; - cursor: pointer; - z-index: 10; - transition: all 0.2s ease; - backdrop-filter: blur(4px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - opacity: 0; - display: flex; - align-items: center; - justify-content: center; - line-height: 1; -} - -.grid-item:hover .metadata-btn { - opacity: 1; -} - -.metadata-btn:hover { - background: rgba(0, 0, 0, 0.9); - transform: scale(1.1); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); -} - -.metadata-btn:active { - transform: scale(0.95); -} - -/* Grid item positioning for metadata button */ -.grid-item { - position: relative; -} - -/* Metadata popup */ -.metadata-popup { - background: var(--card-background); - border: 1px solid var(--border-color); - border-radius: 8px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); - max-width: 320px; - min-width: 250px; - opacity: 0; - transform: translateY(-10px); - transition: all 0.2s ease; - backdrop-filter: blur(10px); - z-index: 1000; -} - -.metadata-popup.show { - opacity: 1; - transform: translateY(0); -} - -.metadata-popup-header { - padding: 12px 16px; - border-bottom: 1px solid var(--border-color); - background: var(--header-background); - border-radius: 8px 8px 0 0; -} - -.metadata-popup-header h4 { - margin: 0; - font-size: 14px; - font-weight: 600; - color: var(--text-color); - word-break: break-word; -} - -.metadata-popup-content { - padding: 12px 16px; - max-height: 300px; - overflow-y: auto; -} - -.metadata-field { - display: flex; - margin-bottom: 8px; - gap: 8px; - align-items: flex-start; -} - -.metadata-field:last-child { - margin-bottom: 0; -} - -.metadata-key { - font-weight: 500; - color: var(--accent-color); - font-size: 12px; - min-width: 80px; - flex-shrink: 0; -} - -.metadata-value { - font-size: 12px; - color: var(--text-color); - word-break: break-word; - flex: 1; -} - -.metadata-value code { - background: var(--code-background); - padding: 2px 4px; - border-radius: 3px; - font-size: 11px; - font-family: 'Courier New', monospace; -} - -.metadata-tag { - background: var(--accent-color); - color: var(--background-color); - padding: 2px 6px; - border-radius: 12px; - font-size: 10px; - font-weight: 500; - margin-right: 4px; - display: inline-block; -} - -.metadata-more { - color: var(--breadcrumb-color); - font-style: italic; - font-size: 11px; -} - -.no-metadata { - color: var(--breadcrumb-color); - font-style: italic; - margin: 0; - text-align: center; - padding: 20px 0; -} - -/* Custom scrollbar for metadata popup */ -.metadata-popup-content::-webkit-scrollbar { - width: 6px; -} - -.metadata-popup-content::-webkit-scrollbar-track { - background: transparent; -} - -.metadata-popup-content::-webkit-scrollbar-thumb { - background: var(--border-color); - border-radius: 3px; -} - -.metadata-popup-content::-webkit-scrollbar-thumb:hover { - background: var(--accent-color); -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .metadata-popup { - max-width: calc(100vw - 32px); - min-width: 200px; - } - - .metadata-btn { - width: 28px; - height: 28px; - font-size: 14px; - top: 6px; - right: 6px; - } -} diff --git a/assets/css/navigation.css b/assets/css/navigation.css deleted file mode 100644 index 1dc5629..0000000 --- a/assets/css/navigation.css +++ /dev/null @@ -1,57 +0,0 @@ -/* ======================================== - NAVIGATION COMPONENTS - ======================================== */ -.breadcrumb { - margin: 0.5rem 0 1rem 0; - font-size: 0.9rem; - color: var(--breadcrumb-color); -} - -.breadcrumb a { - color: var(--link-color); - text-decoration: none; -} - -.breadcrumb a:hover { - text-decoration: underline; - color: var(--link-hover); -} - -.breadcrumb .separator { - margin: 0 0.3rem; - color: var(--breadcrumb-color); - opacity: 0.7; -} - -.navigation { - margin: 1rem 0; - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -.nav-btn { - background: var(--button-bg); - color: white; - padding: 0.5rem 1rem; - border: none; - border-radius: 6px; - cursor: pointer; - text-decoration: none; - transition: all 0.2s ease; - font-size: 0.9rem; - display: inline-flex; - align-items: center; - gap: 0.3rem; -} - -.nav-btn:hover { - background: var(--button-hover); - transform: translateY(-1px); -} - -.nav-btn:disabled { - background: var(--disabled-color); - cursor: not-allowed; - transform: none; -} diff --git a/assets/css/responsive.css b/assets/css/responsive.css deleted file mode 100644 index 1ca81f5..0000000 --- a/assets/css/responsive.css +++ /dev/null @@ -1,59 +0,0 @@ -/* ======================================== - RESPONSIVE DESIGN - ======================================== */ -@media (max-width: 768px) { - body { - padding: 0.5rem; - } - - .grid { - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - gap: 0.8rem; - } - - .sidebar { - width: 100%; - right: -100%; - } - - .navigation { - gap: 0.3rem; - } - - .nav-btn { - padding: 0.4rem 0.8rem; - font-size: 0.8rem; - } - - .search-container { - max-width: 100%; - } - - /* View controls responsive */ - .controls-container { - flex-direction: column; - gap: 1rem; - align-items: stretch; - } - - .sort-controls { - justify-content: center; - } - - .view-controls { - margin-right: 0.5rem !important; - padding: 8px 12px !important; - justify-content: center; - } - - .view-btn { - min-width: 40px !important; - min-height: 40px !important; - padding: 8px 12px !important; - } - - .view-btn svg { - width: 16px !important; - height: 16px !important; - } -} diff --git a/assets/css/search.css b/assets/css/search.css deleted file mode 100644 index 7bd6995..0000000 --- a/assets/css/search.css +++ /dev/null @@ -1,72 +0,0 @@ -/* ======================================== - SEARCH FUNCTIONALITY - ======================================== */ -.search-container { - position: relative; - max-width: 500px; - margin: 1rem 0; -} - -.search-box { - width: 100%; - padding: 0.8rem 3rem 0.8rem 1rem; - border: 1px solid var(--border-color); - border-radius: 8px; - background: var(--card-bg); - color: var(--text-color); - font-size: 1rem; - outline: none; - transition: all 0.3s ease; -} - -.search-box:focus { - border-color: var(--button-bg); - box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.1); -} - -.search-icon { - position: absolute; - right: 1rem; - top: 50%; - transform: translateY(-50%); - color: var(--text-color); - opacity: 0.6; - pointer-events: none; -} - -.search-results { - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - margin-top: 0.5rem; - max-height: 400px; - overflow-y: auto; - display: none; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - z-index: 100; -} - -.search-result-item { - padding: 0.75rem; - margin: 0; - cursor: pointer; - transition: background-color 0.2s ease; - border-bottom: 1px solid var(--border-color); -} - -.search-result-item:last-child { - border-bottom: none; -} - -.search-result-item:hover { - background: var(--button-bg); - color: white; -} - -.search-highlight { - background: #ffeb3b; - color: #000; - font-weight: bold; - padding: 0.1rem 0.2rem; - border-radius: 2px; -} diff --git a/assets/css/sidebar.css b/assets/css/sidebar.css deleted file mode 100644 index 7718fb1..0000000 --- a/assets/css/sidebar.css +++ /dev/null @@ -1,121 +0,0 @@ -/* ======================================== - SIDEBAR (RECENT PLOTS) - ======================================== */ -.sidebar { - position: fixed; - top: 0; - right: -350px; - width: 330px; - height: 100vh; - background: var(--card-bg); - border-left: 2px solid var(--border-color); - z-index: 2000; - transition: right 0.3s ease; - overflow-y: auto; - box-shadow: -4px 0 15px rgba(0,0,0,0.2); -} - -.sidebar.open { - right: 0; -} - -.sidebar-header { - padding: 1.2rem; - border-bottom: 1px solid var(--border-color); - position: sticky; - top: 0; - background: var(--card-bg); - z-index: 1; -} - -.sidebar-title { - margin: 0; - font-size: 1.2rem; - color: var(--text-color); - font-weight: 600; -} - -.sidebar-close { - position: absolute; - top: 1rem; - right: 1rem; - background: none; - border: none; - font-size: 1.5rem; - cursor: pointer; - color: var(--text-color); - padding: 0.2rem; - border-radius: 4px; - transition: background-color 0.2s ease; -} - -.sidebar-close:hover { - background: var(--border-color); -} - -.sidebar-content { - padding: 1rem; -} - -.recent-plot { - display: flex; - gap: 0.7rem; - padding: 0.8rem; - margin-bottom: 0.8rem; - border-radius: 6px; - cursor: pointer; - transition: all 0.2s ease; - border: 1px solid var(--border-color); -} - -.recent-plot:hover { - background: var(--button-bg); - color: white; - transform: translateX(2px); -} - -.recent-plot-thumb { - width: 60px; - height: 48px; - object-fit: cover; - border-radius: 4px; - flex-shrink: 0; -} - -.recent-plot-info { - flex: 1; - min-width: 0; -} - -.recent-plot-name { - font-size: 0.9rem; - font-weight: 600; - margin-bottom: 0.3rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.recent-plot-path { - font-size: 0.8rem; - color: var(--breadcrumb-color); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.sidebar-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0,0,0,0.5); - z-index: 1500; - display: none; - backdrop-filter: blur(2px); -} - -.sidebar-overlay.open { - display: block; -} diff --git a/assets/css/sort-controls.css b/assets/css/sort-controls.css deleted file mode 100644 index 3df2439..0000000 --- a/assets/css/sort-controls.css +++ /dev/null @@ -1,88 +0,0 @@ -/* ======================================== - SORT CONTROLS STYLING - ======================================== */ - -/* Clean styling for sort controls */ -.sort-controls { - display: flex !important; - align-items: center !important; - gap: 8px !important; - padding: 12px 16px !important; - background: var(--tree-bg) !important; - border: 1px solid var(--border-color) !important; - border-radius: 8px !important; - margin-right: 1rem !important; -} - -.sort-label { - font-size: 0.9rem !important; - color: var(--text-color) !important; - margin-right: 8px !important; - font-weight: 500 !important; -} - -/* Button styling using theme variables */ -.sort-btn { - background: var(--card-bg) !important; - border: 1px solid var(--border-color) !important; - border-radius: 6px !important; - padding: 8px 12px !important; - cursor: pointer !important; - transition: all 0.2s ease !important; - font-size: 0.85rem !important; - color: var(--text-color) !important; - display: flex !important; - align-items: center !important; - gap: 4px !important; - outline: none !important; - text-decoration: none !important; - font-family: inherit !important; -} - -.sort-btn:hover { - background: var(--button-bg) !important; - color: white !important; - transform: translateY(-1px) !important; - box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; - border-color: var(--button-bg) !important; -} - -.sort-btn.active { - background: var(--button-bg) !important; - color: white !important; - border-color: var(--button-bg) !important; - box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; -} - -.sort-order-btn { - background: var(--card-bg) !important; - border: 1px solid var(--border-color) !important; - border-radius: 6px !important; - padding: 8px 12px !important; - cursor: pointer !important; - transition: all 0.2s ease !important; - font-size: 1rem !important; - color: var(--text-color) !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - min-width: 36px !important; - outline: none !important; - text-decoration: none !important; - font-family: inherit !important; -} - -.sort-order-btn:hover { - background: var(--button-bg) !important; - color: white !important; - transform: translateY(-1px) !important; - box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; - border-color: var(--button-bg) !important; -} - -.sort-order-btn.active { - background: var(--button-bg) !important; - color: white !important; - border-color: var(--button-bg) !important; - box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; -} diff --git a/assets/css/sort-debug.css b/assets/css/sort-debug.css deleted file mode 100644 index e69de29..0000000 diff --git a/assets/css/stats.css b/assets/css/stats.css deleted file mode 100644 index 10868e5..0000000 --- a/assets/css/stats.css +++ /dev/null @@ -1,59 +0,0 @@ -/* ======================================== - GALLERY STATISTICS - ======================================== */ -.gallery-stats { - position: fixed; - bottom: 80px; - left: 15px; - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 0.8rem 1rem; - font-size: 0.85rem; - color: var(--breadcrumb-color); - box-shadow: 0 2px 8px rgba(0,0,0,0.1); - z-index: 500; - opacity: 0.8; - transition: opacity 0.2s ease; - max-width: 200px; - /* Ensure stats don't interfere with content */ - pointer-events: none; -} - -.gallery-stats:hover { - opacity: 1; - /* Re-enable pointer events on hover */ - pointer-events: auto; -} - -.stats-item { - display: flex; - justify-content: space-between; - align-items: center; - margin: 0.2rem 0; - white-space: nowrap; -} - -.stats-label { - margin-right: 0.8rem; -} - -.stats-value { - font-weight: 600; - color: var(--text-color); -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .gallery-stats { - bottom: 60px; - left: 10px; - padding: 0.6rem 0.8rem; - font-size: 0.8rem; - max-width: 180px; - } - - .stats-item { - margin: 0.15rem 0; - } -} diff --git a/assets/css/variables.css b/assets/css/variables.css deleted file mode 100644 index 72c531e..0000000 --- a/assets/css/variables.css +++ /dev/null @@ -1,33 +0,0 @@ -/* ======================================== - CSS VARIABLES AND THEME DEFINITIONS - ======================================== */ -:root { - --bg-color: #1e1e1e; - --text-color: #ffffff; - --card-bg: #2d2d2d; - --border-color: #404040; - --link-color: #569cd6; - --link-hover: #4a9eff; - --button-bg: #0078d4; - --button-hover: #106ebe; - --breadcrumb-color: #cccccc; - --tree-bg: #252526; - --tree-current-bg: #0078d4; - --success-color: #28a745; - --success-hover: #218838; - --disabled-color: #6c757d; -} - -[data-theme="light"] { - --bg-color: #ffffff; - --text-color: #333333; - --card-bg: #f8f8f8; - --border-color: #ddd; - --link-color: #007acc; - --link-hover: #005a9e; - --button-bg: #007acc; - --button-hover: #005a9e; - --breadcrumb-color: #666; - --tree-bg: #f8f8f8; - --tree-current-bg: #007acc; -} diff --git a/assets/css/view-controls.css b/assets/css/view-controls.css deleted file mode 100644 index 8e7a9b9..0000000 --- a/assets/css/view-controls.css +++ /dev/null @@ -1,404 +0,0 @@ -/* ======================================== - VIEW CONTROLS AND LAYOUT MODES - ======================================== */ - -/* Controls Container */ -.controls-container { - display: flex; - justify-content: space-between; - align-items: center; - margin: 1rem 0; - gap: 2rem; - flex-wrap: wrap; -} - -/* Sort Controls */ -.sort-controls { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - background: var(--tree-bg); - border: 1px solid var(--border-color); - border-radius: 8px; -} - -.sort-label { - font-size: 0.9rem; - color: var(--text-color); - margin-right: 4px; - font-weight: 500; -} - -.sort-btn, .sort-order-btn { - background: var(--card-bg) !important; - border: 1px solid var(--border-color) !important; - border-radius: 6px !important; - padding: 6px 12px !important; - cursor: pointer !important; - transition: all 0.2s ease !important; - font-size: 0.85rem !important; - color: var(--text-color) !important; - display: flex !important; - align-items: center !important; - gap: 4px !important; - outline: none !important; - text-decoration: none !important; -} - -.sort-btn:hover, .sort-order-btn:hover { - background: var(--button-bg) !important; - color: white !important; - transform: translateY(-1px) !important; - box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; -} - -.sort-btn.active, .sort-order-btn.active { - background: var(--button-bg) !important; - color: white !important; - border-color: var(--button-bg) !important; - box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; -} - -.sort-order-btn { - min-width: 32px !important; - justify-content: center !important; - font-size: 1rem !important; -} - -/* View Controls */ -.view-controls { - display: flex; - justify-content: flex-end; - align-items: center; - gap: 12px; - padding: 12px 16px; - background: var(--tree-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - position: relative; - z-index: 10; -} - -.view-btn { - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 12px 16px; - cursor: pointer; - transition: all 0.2s ease; - font-size: 1.2rem; - color: var(--text-color); - min-width: 48px; - min-height: 48px; - display: flex; - align-items: center; - justify-content: center; -} - -.view-btn svg { - width: 18px; - height: 18px; -} - -.view-btn:hover { - background: var(--button-bg); - color: white; - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); -} - -.view-btn.active { - background: var(--button-bg); - color: white; - border-color: var(--button-bg); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -/* Plot Container Base Styles */ -.plot-container { - margin: 1rem 0; - margin-bottom: 2rem; /* Reduced from 450px - stats box is fixed positioned */ - transition: all 0.3s ease; - clear: both; -} - -.plot-container .plot-item { - transition: all 0.2s ease; - border-radius: 8px; - overflow: hidden; - background: var(--card-bg); - border: 1px solid transparent; -} - -.plot-container .plot-item:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0,0,0,0.1); - border-color: var(--border-color); -} - -.plot-container .plot-link { - color: var(--link-color); - text-decoration: none; - display: block; -} - -.plot-container .plot-thumbnail { - width: 100%; - height: auto; - border-radius: 6px; - transition: all 0.2s ease; -} - -.plot-container .plot-info { - padding: 0.8rem; -} - -.plot-container .plot-name { - word-wrap: break-word; - word-break: break-word; - hyphens: auto; - font-size: 0.9rem; - line-height: 1.3; - font-weight: 500; - color: var(--text-color); -} - -.plot-container .plot-date { - font-size: 0.8rem; - color: var(--text-secondary); - margin-top: 0.3rem; - opacity: 0.7; /* Slightly lower opacity for differentiation */ -} - -/* Grid View - Override any conflicting styles */ -.plot-container.grid-view { - display: grid !important; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)) !important; - gap: 1.2rem !important; - padding: 1rem 0 !important; -} - -.plot-container.grid-view .plot-item, -.plot-container.grid-view .grid-item { - text-align: center !important; - background: var(--card-bg) !important; - border-radius: 8px !important; - padding: 0.8rem !important; - transition: all 0.2s ease !important; - border: 1px solid transparent !important; - display: block !important; - width: auto !important; - max-width: none !important; -} - -.plot-container.grid-view .plot-item:hover, -.plot-container.grid-view .grid-item:hover { - transform: translateY(-2px) !important; - box-shadow: 0 4px 12px rgba(0,0,0,0.1) !important; - border-color: var(--border-color) !important; -} - -.plot-container.grid-view .plot-link { - display: block !important; - color: var(--link-color) !important; - text-decoration: none !important; -} - -.plot-container.grid-view .plot-thumbnail { - max-width: 100% !important; - height: auto !important; - border: 1px solid var(--border-color) !important; - display: block !important; - width: 100% !important; - object-fit: contain !important; - border-radius: 6px !important; -} - -.plot-container.grid-view .plot-info { - padding: 0.8rem 0 0 0 !important; -} - -.plot-container.grid-view .plot-name { - max-height: 3.9rem !important; - overflow: hidden !important; - margin-top: 0.8rem !important; - display: block !important; - word-wrap: break-word !important; - word-break: break-word !important; - hyphens: auto !important; - font-size: 0.9rem !important; - line-height: 1.3 !important; - font-weight: 500 !important; - color: var(--text-color) !important; - text-align: center !important; -} - -/* Large List View */ -.plot-container.list-large-view { - display: flex !important; - flex-direction: column !important; - gap: 0.8rem !important; -} - -.plot-container.list-large-view .plot-item { - display: flex !important; - align-items: center !important; - padding: 1rem !important; - gap: 1rem !important; -} - -.plot-container.list-large-view .plot-link { - flex-shrink: 0 !important; - width: 120px !important; - height: 90px !important; - overflow: hidden !important; - border-radius: 6px !important; - border: 1px solid var(--border-color) !important; -} - -.plot-container.list-large-view .plot-thumbnail { - width: 100% !important; - height: 100% !important; - object-fit: cover !important; -} - -.plot-container.list-large-view .plot-info { - flex: 1 !important; - padding: 0 !important; - text-align: left !important; - display: flex !important; - justify-content: space-between !important; - align-items: center !important; -} - -.plot-container.list-large-view .plot-name { - font-size: 1rem !important; - line-height: 1.4 !important; - max-height: none !important; - overflow: visible !important; - flex: 1 !important; -} - -.plot-container.list-large-view .plot-date { - flex-shrink: 0 !important; - margin-left: 1rem !important; - margin-top: 0 !important; - font-size: 0.85rem !important; - color: var(--text-secondary) !important; - white-space: nowrap !important; -} - -/* Compact List View */ -.plot-container.list-compact-view { - display: flex !important; - flex-direction: column !important; - gap: 0.4rem !important; -} - -.plot-container.list-compact-view .plot-item { - display: flex !important; - align-items: center !important; - padding: 0.6rem 1rem !important; - gap: 0.8rem !important; - border-radius: 4px !important; -} - -.plot-container.list-compact-view .plot-link { - flex: 1 !important; - display: flex !important; - align-items: center !important; -} - -.plot-container.list-compact-view .plot-thumbnail { - display: none !important; -} - -.plot-container.list-compact-view .plot-info { - padding: 0 !important; - flex: 1 !important; - text-align: left !important; - display: flex !important; - justify-content: space-between !important; - align-items: center !important; -} - -.plot-container.list-compact-view .plot-name { - font-size: 0.95rem !important; - line-height: 1.2 !important; - margin: 0 !important; - white-space: nowrap !important; - overflow: hidden !important; - text-overflow: ellipsis !important; - flex: 1 !important; -} - -.plot-container.list-compact-view .plot-date { - flex-shrink: 0 !important; - margin-left: 1rem !important; - margin-top: 0 !important; - font-size: 0.8rem !important; - color: var(--text-secondary) !important; - white-space: nowrap !important; -} - -/* Highlight effect for all views */ -.plot-item.highlighted { - border-color: var(--button-bg); - box-shadow: 0 0 15px rgba(0, 120, 212, 0.3); - transform: translateY(-2px); - animation: highlightPulse 2s ease-in-out; -} - -@keyframes highlightPulse { - 0%, 100% { transform: translateY(-2px) scale(1); } - 50% { transform: translateY(-2px) scale(1.02); } -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .view-controls { - width: 100%; - margin-left: 0; - margin-right: 0; - } - - .plot-container.grid-view { - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - gap: 1rem; - } - - .plot-container.list-large-view .plot-link { - width: 80px; - height: 60px; - } - - .plot-container.list-large-view .plot-item { - padding: 0.8rem; - } - - .plot-container.list-compact-view .plot-item { - padding: 0.5rem 0.8rem; - } -} - -@media (max-width: 480px) { - .view-controls { - gap: 4px; - } - - .view-btn { - padding: 6px 8px; - font-size: 1rem; - min-width: 32px; - } - - .plot-container.grid-view { - grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); - } - - .plot-container.list-large-view .plot-link { - width: 60px; - height: 45px; - } -} diff --git a/assets/css/view-override.css b/assets/css/view-override.css deleted file mode 100644 index e9bd47c..0000000 --- a/assets/css/view-override.css +++ /dev/null @@ -1,54 +0,0 @@ -/* ======================================== - VIEW OVERRIDE - Ensure grid view works - ======================================== */ - -/* Force grid layout when grid-view class is present */ -body .plot-container.grid-view { - display: grid !important; - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)) !important; - gap: 1.2rem !important; - padding: 1rem 0 !important; -} - -/* Force grid items to be proper tiles */ -body .plot-container.grid-view .plot-item, -body .plot-container.grid-view .grid-item { - display: block !important; - width: auto !important; - max-width: none !important; - text-align: center !important; - background: var(--card-bg) !important; - border-radius: 8px !important; - padding: 0.8rem !important; - border: 1px solid transparent !important; -} - -/* Ensure thumbnails are properly sized */ -body .plot-container.grid-view .plot-thumbnail { - width: 100% !important; - height: auto !important; - max-width: 100% !important; - display: block !important; - border: 1px solid var(--border-color) !important; - border-radius: 6px !important; - object-fit: cover !important; - aspect-ratio: 4/3; -} - -/* Force plot info styling */ -body .plot-container.grid-view .plot-info { - padding: 0.5rem 0 0 0 !important; - text-align: center !important; -} - -body .plot-container.grid-view .plot-name { - font-size: 0.85rem !important; - line-height: 1.2 !important; - max-height: 2.4rem !important; - overflow: hidden !important; - text-overflow: ellipsis !important; - display: -webkit-box !important; - -webkit-line-clamp: 2 !important; - line-clamp: 2 !important; - -webkit-box-orient: vertical !important; -} diff --git a/assets/js/comparison-manager.js b/assets/js/comparison-manager.js deleted file mode 100644 index 7056775..0000000 --- a/assets/js/comparison-manager.js +++ /dev/null @@ -1,180 +0,0 @@ -/** - * Plot comparison functionality - */ -export class ComparisonManager { - constructor() { - this.comparisonMode = false; - this.comparisonSlot = null; // 'left' or 'right' - this.plots = { left: null, right: null }; - } - - /** - * Toggle comparison mode - */ - toggleCompareMode() { - this.comparisonMode = !this.comparisonMode; - const compareBtn = document.getElementById('compareToggle'); - - if (!compareBtn) return; - - if (this.comparisonMode) { - compareBtn.style.background = 'var(--success-color)'; - compareBtn.title = 'Exit Compare Mode (Ctrl+C)'; - this.showComparisonOverlay(); - } else { - compareBtn.style.background = 'var(--button-bg)'; - compareBtn.title = 'Compare Plots (Ctrl+C)'; - this.hideComparisonOverlay(); - } - } - - /** - * Show comparison overlay - */ - showComparisonOverlay() { - const overlay = document.getElementById('comparisonOverlay'); - if (overlay) overlay.classList.add('open'); - } - - /** - * Hide comparison overlay - */ - hideComparisonOverlay() { - const overlay = document.getElementById('comparisonOverlay'); - if (overlay) overlay.classList.remove('open'); - this.comparisonMode = false; - - const compareBtn = document.getElementById('compareToggle'); - if (compareBtn) { - compareBtn.style.background = 'var(--button-bg)'; - compareBtn.title = 'Compare Plots (Ctrl+C)'; - } - } - - /** - * Close comparison overlay (alias for hideComparisonOverlay) - */ - closeComparison() { - this.hideComparisonOverlay(); - } - - /** - * Select plot for comparison - simple approach - */ - selectPlotForComparison(slot) { - this.comparisonSlot = slot; - - // Hide overlay temporarily by removing the 'open' class - const overlay = document.getElementById('comparisonOverlay'); - if (overlay) overlay.classList.remove('open'); - - // Show simple alert with instructions - const instruction = document.createElement('div'); - instruction.id = 'comparisonInstruction'; - instruction.style.cssText = ` - position: fixed; - top: 20px; - left: 50%; - transform: translateX(-50%); - background: var(--button-bg); - color: white; - padding: 1rem 2rem; - border-radius: 8px; - z-index: 3000; - font-size: 1.1rem; - font-weight: 600; - box-shadow: 0 4px 20px rgba(0,0,0,0.3); - `; - instruction.innerHTML = `📊 Click any plot to select for ${slot === 'left' ? 'Plot A' : 'Plot B'} (ESC to cancel)`; - document.body.appendChild(instruction); - - // Add one-time click listener to all grid items - const gridItems = document.querySelectorAll('.grid-item'); - const handleClick = (event) => { - event.preventDefault(); - event.stopPropagation(); - - const gridItem = event.currentTarget; - const img = gridItem.querySelector('img'); - const nameEl = gridItem.querySelector('.plot-name'); - - if (img && nameEl) { - const plotInfo = { - name: nameEl.textContent.trim(), - imgSrc: img.src, - pdfSrc: img.src.replace('.png', '.pdf'), - path: window.location.pathname - }; - - this.addPlotToComparison(plotInfo, slot); - } - - // Clean up - instruction.remove(); - gridItems.forEach(item => item.removeEventListener('click', handleClick)); - this.comparisonSlot = null; - if (overlay) overlay.classList.add('open'); - }; - - gridItems.forEach(item => { - item.style.cursor = 'pointer'; - item.style.border = '2px dashed var(--button-bg)'; - item.addEventListener('click', handleClick); - }); - - // ESC to cancel - const handleEscape = (event) => { - if (event.key === 'Escape') { - instruction.remove(); - gridItems.forEach(item => { - item.removeEventListener('click', handleClick); - item.style.cursor = ''; - item.style.border = ''; - }); - this.comparisonSlot = null; - if (overlay) overlay.classList.add('open'); - document.removeEventListener('keydown', handleEscape); - } - }; - document.addEventListener('keydown', handleEscape); - } - - /** - * Add plot to comparison panel - */ - addPlotToComparison(plotInfo, slot) { - this.plots[slot] = plotInfo; - - const container = document.getElementById(`${slot}PlotContainer`); - const title = document.getElementById(`${slot}PlotTitle`); - const replaceBtn = document.getElementById(`${slot}ReplaceBtn`); - - if (container) { - container.innerHTML = ` - ${plotInfo.name} -
- ${plotInfo.name}
- ${plotInfo.path} -
- `; - } - - if (title) title.textContent = plotInfo.name; - if (replaceBtn) replaceBtn.style.display = 'block'; - - // Reset grid item styles - const gridItems = document.querySelectorAll('.grid-item'); - gridItems.forEach(item => { - item.style.cursor = ''; - item.style.border = ''; - }); - } - - /** - * Replace plot in comparison - */ - replacePlot(slot) { - this.selectPlotForComparison(slot); - } -} diff --git a/assets/js/export-manager.js b/assets/js/export-manager.js deleted file mode 100644 index b60967b..0000000 --- a/assets/js/export-manager.js +++ /dev/null @@ -1,448 +0,0 @@ -/** - * Export Manager for Gallery - * Handles exporting selected plots to merged PDF - */ - -export class ExportManager { - constructor() { - this.selectedPlots = new Set(); - this.maxPlots = 4; - this.init(); - } - - init() { - this.createExportButton(); - this.bindEvents(); - } - - /** - * Create the export button in the floating buttons section - */ - createExportButton() { - const floatingButtons = document.querySelector('.floating-buttons'); - if (!floatingButtons) return; - - const exportBtn = document.createElement('button'); - exportBtn.className = 'floating-btn export-btn'; - exportBtn.id = 'exportBtn'; - exportBtn.title = 'Export Selected Plots (Ctrl+E)'; - exportBtn.innerHTML = '📄'; - exportBtn.style.display = 'none'; // Hidden by default - exportBtn.onclick = () => this.exportSelectedPlots(); - - floatingButtons.appendChild(exportBtn); - - // Add selection counter - const selectionCounter = document.createElement('div'); - selectionCounter.className = 'selection-counter'; - selectionCounter.id = 'selectionCounter'; - selectionCounter.style.display = 'none'; - selectionCounter.innerHTML = '0/4 selected'; - floatingButtons.appendChild(selectionCounter); - } - - /** - * Bind events for plot selection - */ - bindEvents() { - // Add selection mode toggle - document.addEventListener('keydown', (e) => { - if (e.ctrlKey && e.key === 'e') { - e.preventDefault(); - this.toggleSelectionMode(); - } - if (e.key === 'Escape') { - this.exitSelectionMode(); - } - }); - - // Add selection handlers to existing plots - this.addSelectionHandlers(); - } - - /** - * Add selection handlers to all plot items - */ - addSelectionHandlers() { - const plotItems = document.querySelectorAll('.grid-item'); - plotItems.forEach(item => this.addSelectionHandler(item)); - } - - /** - * Add selection handler to a single plot item - */ - addSelectionHandler(item) { - // Create selection overlay - const overlay = document.createElement('div'); - overlay.className = 'selection-overlay'; - overlay.innerHTML = ` -
- -
- `; - item.appendChild(overlay); - - // Add click handler for selection - overlay.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - this.togglePlotSelection(item); - }); - } - - /** - * Toggle selection mode - */ - toggleSelectionMode() { - const body = document.body; - const isSelectionMode = body.classList.contains('selection-mode'); - - if (isSelectionMode) { - this.exitSelectionMode(); - } else { - this.enterSelectionMode(); - } - } - - /** - * Enter selection mode - */ - enterSelectionMode() { - document.body.classList.add('selection-mode'); - document.getElementById('exportBtn').style.display = 'block'; - document.getElementById('selectionCounter').style.display = 'block'; - this.updateSelectionCounter(); - } - - /** - * Exit selection mode - */ - exitSelectionMode() { - const wasInSelectionMode = document.body.classList.contains('selection-mode'); - - document.body.classList.remove('selection-mode'); - document.getElementById('exportBtn').style.display = 'none'; - document.getElementById('selectionCounter').style.display = 'none'; - this.clearSelection(); - - // Show message if user was actually in selection mode - if (wasInSelectionMode) { - this.showMessage('Exited selection mode', 'info'); - } - } - - /** - * Toggle plot selection - */ - togglePlotSelection(item) { - const plotName = this.getPlotName(item); - const plotPath = this.getPlotPath(item); - - if (this.selectedPlots.has(plotName)) { - this.selectedPlots.delete(plotName); - item.classList.remove('selected'); - item.querySelector('.checkbox-icon').textContent = '☐'; - } else { - if (this.selectedPlots.size >= this.maxPlots) { - this.showMessage(`Maximum ${this.maxPlots} plots can be selected`, 'warning'); - return; - } - this.selectedPlots.add(plotName); - item.classList.add('selected'); - item.querySelector('.checkbox-icon').textContent = '☑'; - } - - this.updateSelectionCounter(); - } - - /** - * Get plot name from grid item - */ - getPlotName(item) { - const plotName = item.querySelector('.plot-name'); - return plotName ? plotName.textContent.trim() : ''; - } - - /** - * Get plot PDF path from grid item - */ - getPlotPath(item) { - const link = item.querySelector('a[href$=".pdf"]'); - return link ? link.href : ''; - } - - /** - * Update selection counter - */ - updateSelectionCounter() { - const counter = document.getElementById('selectionCounter'); - if (counter) { - counter.textContent = `${this.selectedPlots.size}/${this.maxPlots} selected`; - } - - const exportBtn = document.getElementById('exportBtn'); - if (exportBtn) { - exportBtn.disabled = this.selectedPlots.size === 0; - exportBtn.style.opacity = this.selectedPlots.size === 0 ? '0.5' : '1'; - } - } - - /** - * Clear all selections - */ - clearSelection() { - this.selectedPlots.clear(); - document.querySelectorAll('.grid-item.selected').forEach(item => { - item.classList.remove('selected'); - const checkbox = item.querySelector('.checkbox-icon'); - if (checkbox) checkbox.textContent = '☐'; - }); - this.updateSelectionCounter(); - } - - /** - * Export selected plots to merged PDF - */ - async exportSelectedPlots() { - if (this.selectedPlots.size === 0) { - this.showMessage('No plots selected', 'warning'); - return; - } - - const plotPaths = Array.from(this.selectedPlots).map(plotName => { - const item = Array.from(document.querySelectorAll('.grid-item')) - .find(item => this.getPlotName(item) === plotName); - return this.getPlotPath(item); - }); - - this.showMessage('Preparing export...', 'info'); - - try { - await this.createMergedPDF(plotPaths); - } catch (error) { - this.showMessage('Export failed: ' + error.message, 'error'); - } - } - - /** - * Create merged PDF using Python script - */ - async createMergedPDF(plotPaths) { - // Convert file:// URLs to actual paths - const actualPaths = plotPaths.map(url => { - if (url.startsWith('file://')) { - return url.substring(7); // Remove 'file://' prefix - } - return url; - }); - - const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0]; - const outputName = `merged_plots_${timestamp}.pdf`; - - const payload = { - plots: actualPaths, - layout: this.calculateLayout(actualPaths.length), - output_name: outputName - }; - - // Generate a unique temporary filename - const tempFileName = `export_request_${Date.now()}.json`; - - // Get work directory from config or fallback - const workDir = window.galleryConfig?.workDir || '/work/kschmidt/web'; - - // Save the request to a JSON file that can be picked up by a Python script - const requestData = JSON.stringify(payload, null, 2); - - // Show improved export instructions with full command - this.showExportInstructions(requestData, tempFileName, workDir); - } - - /** - * Calculate optimal layout for given number of plots - */ - calculateLayout(numPlots) { - switch (numPlots) { - case 1: return { rows: 1, cols: 1 }; - case 2: return { rows: 1, cols: 2 }; - case 3: return { rows: 2, cols: 2 }; // 3 plots in 2x2 grid with one empty - case 4: return { rows: 2, cols: 2 }; - default: return { rows: 2, cols: 2 }; - } - } - - /** - * Show export instructions to user - */ - showExportInstructions(requestData, tempFileName, workDir) { - const tempFilePath = `/tmp/${tempFileName}`; - const fullCommand = `echo '${requestData.replace(/'/g, "'\\''")}' > ${tempFilePath} && cd ${workDir} && python export_plots.py ${tempFilePath}`; - - const instructions = ` -
-

🚀 Export Selected Plots

-

Run the following command in your terminal to export the selected plots:

- -
-
- ${fullCommand} -
-
- - - -
-
- -
-

📋 Command Breakdown:

-
    -
  • Creates temporary file: ${tempFilePath}
  • -
  • Changes to work directory: ${workDir}
  • -
  • Runs export script: python export_plots.py
  • -
  • Output file: Will be saved in the work directory
  • -
-
- -
-

💡 Tips:

-
    -
  • The temporary JSON file will be automatically cleaned up after successful export
  • -
  • Use Esc to exit selection mode
  • -
  • Press Ctrl+E to toggle selection mode
  • -
-
-
- `; - - const overlay = document.createElement('div'); - overlay.className = 'export-overlay'; - overlay.innerHTML = instructions; - - // Add methods to the overlay for button handlers - overlay.copyCommand = function() { - navigator.clipboard.writeText(fullCommand).then(() => { - this.showCopyFeedback('Command copied to clipboard!'); - }).catch(() => { - this.showCopyFeedback('Failed to copy. Please select and copy manually.', 'error'); - }); - }; - - overlay.copyJSON = function() { - navigator.clipboard.writeText(requestData).then(() => { - this.showCopyFeedback('JSON copied to clipboard!'); - }).catch(() => { - this.showCopyFeedback('Failed to copy. Please select and copy manually.', 'error'); - }); - }; - - overlay.close = function() { - this.remove(); - }; - - overlay.showCopyFeedback = function(message, type = 'success') { - const feedback = document.createElement('div'); - feedback.className = `copy-feedback copy-feedback-${type}`; - feedback.textContent = message; - this.appendChild(feedback); - - setTimeout(() => { - if (feedback.parentNode) { - feedback.parentNode.removeChild(feedback); - } - }, 2000); - }; - - document.body.appendChild(overlay); - - // Close on ESC key - const handleEscape = (e) => { - if (e.key === 'Escape') { - overlay.remove(); - document.removeEventListener('keydown', handleEscape); - } - }; - document.addEventListener('keydown', handleEscape); - - // Close on clicking outside - overlay.addEventListener('click', (e) => { - if (e.target === overlay) { - overlay.remove(); - document.removeEventListener('keydown', handleEscape); - } - }); - } - - /** - * Download the PDF blob - */ - downloadPDF(blob) { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `merged_plots_${new Date().toISOString().split('T')[0]}.pdf`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - } - - /** - * Show temporary message to user - */ - showMessage(text, type = 'info') { - // Remove existing message - const existing = document.querySelector('.export-message'); - if (existing) existing.remove(); - - const message = document.createElement('div'); - message.className = `export-message export-message-${type}`; - message.textContent = text; - - document.body.appendChild(message); - - setTimeout(() => { - if (message.parentNode) { - message.parentNode.removeChild(message); - } - }, 3000); - } -} - -// Add these methods to ExportManager if not present -ExportManager.prototype.isSelectionModeActive = function() { - return document.body.classList.contains('selection-mode'); -}; -ExportManager.prototype.exitSelectionMode = function() { - document.body.classList.remove('selection-mode'); - if (typeof this.clearSelection === 'function') { - this.clearSelection(); - } -}; - -// Ensure a single global instance -window.exportManager = window.exportManager || new ExportManager(); - -// Listen for ESC key globally to exit selection mode -// (This will work even if focus is not on a plot) -document.addEventListener('keydown', function(e) { - if (e.key === 'Escape' && window.exportManager && window.exportManager.isSelectionModeActive()) { - window.exportManager.exitSelectionMode(); - } -}); - -// Attach improved export logic to export button -document.addEventListener('DOMContentLoaded', function() { - const exportBtn = document.getElementById('exportBtn'); - if (exportBtn) { - exportBtn.addEventListener('click', function() { - window.exportManager.exportSelectedPlots(); - }); - } -}); diff --git a/assets/js/folder-metadata.js b/assets/js/folder-metadata.js deleted file mode 100644 index 86f9db4..0000000 --- a/assets/js/folder-metadata.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Folder Metadata functionality for Gallery - * - * Handles folder metadata dropdown display and interaction - */ - -// Define function immediately (not waiting for DOM) -window.toggleFolderMetadata = function() { - console.log('toggleFolderMetadata called'); - - const container = document.querySelector('.folder-metadata-container'); - const content = document.getElementById('folderMetadataContent'); - - if (!container) { - console.log('No metadata container found'); - return; - } - - if (!content) { - console.log('No metadata content found'); - return; - } - - const isExpanded = container.classList.contains('expanded'); - console.log('Current state - expanded:', isExpanded); - - if (isExpanded) { - // Collapse - container.classList.remove('expanded'); - content.style.display = 'none'; - console.log('Collapsed dropdown'); - } else { - // Expand - container.classList.add('expanded'); - content.style.display = 'block'; - console.log('Expanded dropdown'); - } - - // Save state - localStorage.setItem('folderMetadataExpanded', (!isExpanded).toString()); -}; - -// Also define as regular function for alternative access -function toggleFolderMetadata() { - window.toggleFolderMetadata(); -} - -// Toggle long text display -window.toggleMetadataText = function(button) { - const longText = button.previousElementSibling; - const fullText = button.nextElementSibling; - - if (fullText.style.display === 'none') { - longText.style.display = 'none'; - fullText.style.display = 'inline'; - button.textContent = 'Show less'; - } else { - longText.style.display = 'inline'; - fullText.style.display = 'none'; - button.textContent = 'Show more'; - } -}; - -// Initialize folder metadata on page load -document.addEventListener('DOMContentLoaded', function() { - console.log('Folder metadata script loaded'); - - // Make sure all containers start collapsed - const containers = document.querySelectorAll('.folder-metadata-container'); - console.log('Found', containers.length, 'metadata containers'); - - containers.forEach(container => { - const content = container.querySelector('.folder-metadata-content'); - if (content) { - // Force initial hidden state - container.classList.remove('expanded'); - content.style.display = 'none'; - console.log('Initialized container as collapsed'); - } - }); -}); diff --git a/assets/js/gallery-app.js b/assets/js/gallery-app.js deleted file mode 100644 index 849286e..0000000 --- a/assets/js/gallery-app.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Main Gallery Application - * Orchestrates all the different managers and functionality - */ -import { ThemeManager } from './theme-manager.js'; -import { NavigationManager } from './navigation-manager.js'; -import { SearchManager } from './search-manager.js'; -import { RecentPlotsManager } from './recent-plots-manager.js'; -import { ComparisonManager } from './comparison-manager.js'; -import { StatsManager } from './stats-manager.js'; -import { KeyboardManager } from './keyboard-manager.js'; -import { ViewManager } from './view-manager.js'; -import { SortManager } from './sort-manager.js'; -import { Utils } from './utils.js'; - -/** - * Main Gallery Application Class - */ -export class GalleryApp { - constructor(config = {}) { - // Configuration from backend template variables - this.SEARCH_DEBOUNCE_MS = config.searchDebounceMs || 300; - this.MAX_RECENT_PLOTS = config.maxRecentPlots || 20; - this.stats = config.stats || null; - - // Initialize managers - this.themeManager = new ThemeManager(); - this.navigationManager = new NavigationManager(); - this.searchManager = new SearchManager(this.SEARCH_DEBOUNCE_MS); - this.recentPlotsManager = new RecentPlotsManager(this.MAX_RECENT_PLOTS); - this.comparisonManager = new ComparisonManager(); - this.statsManager = new StatsManager(); - this.viewManager = new ViewManager(); - this.sortManager = new SortManager(); - this.keyboardManager = new KeyboardManager(this); - this.utils = Utils; - - // Set global references for backward compatibility - window.themeManager = this.themeManager; - window.searchManager = this.searchManager; - window.recentPlotsManager = this.recentPlotsManager; - window.comparisonManager = this.comparisonManager; - window.viewManager = this.viewManager; - window.sortManager = this.sortManager; - window.utils = this.utils; - - this.init(); - } - - /** - * Initialize the gallery application - */ - init() { - this.navigationManager.buildBreadcrumb(); - this.navigationManager.buildFolderTree(); - - // Update stats with backend data if available - if (this.stats) { - this.statsManager.updateWithBackendStats(this.stats); - } - - // Handle URL-based thumbnail highlighting - Utils.handleThumbnailHighlight(); - } - - // Backward compatibility methods - toggleTheme() { this.themeManager.toggle(); } - toggleSidebar() { this.recentPlotsManager.toggleSidebar(); } - toggleCompareMode() { this.comparisonManager.toggleCompareMode(); } - closeComparison() { this.comparisonManager.closeComparison(); } - selectPlotForComparison(slot) { this.comparisonManager.selectPlotForComparison(slot); } - replacePlot(slot) { this.comparisonManager.replacePlot(slot); } -} diff --git a/assets/js/keyboard-manager.js b/assets/js/keyboard-manager.js deleted file mode 100644 index c535a9c..0000000 --- a/assets/js/keyboard-manager.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Keyboard shortcuts management - */ -export class KeyboardManager { - constructor(galleryApp) { - this.app = galleryApp; - this.init(); - } - - /** - * Initialize keyboard shortcuts - */ - init() { - document.addEventListener('keydown', (e) => { - if (e.ctrlKey && e.key === 'k') { - e.preventDefault(); - const searchBox = document.getElementById('searchBox'); - if (searchBox) searchBox.focus(); - } - - if (e.ctrlKey && e.key === 'r') { - e.preventDefault(); - if (this.app.recentPlotsManager) { - this.app.recentPlotsManager.toggleSidebar(); - } - } - - if (e.ctrlKey && e.key === 'c') { - e.preventDefault(); - if (this.app.comparisonManager) { - this.app.comparisonManager.toggleCompareMode(); - } - } - - if (e.ctrlKey && e.key === 't') { - e.preventDefault(); - if (this.app.themeManager) { - this.app.themeManager.toggle(); - } - } - - if (e.ctrlKey && e.key === 'v') { - e.preventDefault(); - if (this.app.viewManager) { - this.app.viewManager.cycleView(); - } - } - - if (e.ctrlKey && e.key === 'n') { - e.preventDefault(); - if (this.app.sortManager) { - this.app.sortManager.setSortType('name'); - } - } - - if (e.ctrlKey && e.key === 'm') { - e.preventDefault(); - if (this.app.sortManager) { - this.app.sortManager.setSortType('time'); - } - } - - if (e.ctrlKey && e.key === 'o') { - e.preventDefault(); - if (this.app.sortManager) { - this.app.sortManager.toggleSortOrder(); - } - } - - if (e.key === '?' && !e.ctrlKey && !e.altKey && !e.metaKey) { - e.preventDefault(); - if (this.app.utils && this.app.utils.toggleShortcutsHelp) { - this.app.utils.toggleShortcutsHelp(); - } - } - - if (e.key === 'Escape') { - this.handleEscape(); - } - }); - } - - /** - * Handle escape key actions - */ - handleEscape() { - // If in plot selection mode, cancel it - if (this.app.comparisonManager && this.app.comparisonManager.comparisonSlot) { - // Find and remove instruction element - const instruction = document.getElementById('comparisonInstruction'); - if (instruction) instruction.remove(); - - // Reset grid item styles - const gridItems = document.querySelectorAll('.grid-item'); - gridItems.forEach(item => { - item.style.cursor = ''; - item.style.border = ''; - }); - - // Show overlay again - const comparisonOverlay = document.getElementById('comparisonOverlay'); - if (comparisonOverlay) comparisonOverlay.classList.add('open'); - this.app.comparisonManager.comparisonSlot = null; - return; - } - - // Close comparison overlay if open - const comparisonOverlay = document.getElementById('comparisonOverlay'); - if (comparisonOverlay && comparisonOverlay.classList.contains('open')) { - if (this.app.comparisonManager) { - this.app.comparisonManager.hideComparisonOverlay(); - } - return; - } - - // Other ESC behaviors - const searchResults = document.getElementById('searchResults'); - if (searchResults) searchResults.style.display = 'none'; - - const sidebar = document.getElementById('sidebar'); - if (sidebar && sidebar.classList.contains('open')) { - if (this.app.recentPlotsManager) { - this.app.recentPlotsManager.toggleSidebar(); - } - } - - const shortcutsHelp = document.getElementById('shortcutsHelp'); - if (shortcutsHelp) shortcutsHelp.style.display = 'none'; - } -} diff --git a/assets/js/main.js b/assets/js/main.js deleted file mode 100644 index 8f924e6..0000000 --- a/assets/js/main.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Main entry point for the Gallery application - * This file initializes the app when the DOM is ready - */ -import { GalleryApp } from './gallery-app.js'; - -// Global app instance for backward compatibility -let app; - -// Global functions for onclick handlers (backward compatibility) -function toggleTheme() { app.toggleTheme(); } -function toggleSidebar() { app.toggleSidebar(); } - -// Make functions globally available -window.toggleTheme = toggleTheme; -window.toggleSidebar = toggleSidebar; - -// Initialize application when DOM is ready -document.addEventListener('DOMContentLoaded', function() { - // Configuration will be injected by the template - const config = window.galleryConfig || {}; - - app = new GalleryApp(config); - - // Make app globally available - window.app = app; -}); diff --git a/assets/js/metadata-popup.js b/assets/js/metadata-popup.js deleted file mode 100644 index 085c3a9..0000000 --- a/assets/js/metadata-popup.js +++ /dev/null @@ -1,212 +0,0 @@ -/** - * Metadata Popup functionality for Gallery - * - * Handles showing metadata in small popups overlaid on plot thumbnails - */ - -class MetadataPopup { - constructor() { - this.activePopup = null; - this.init(); - } - - init() { - // Close popup when clicking outside - document.addEventListener('click', (e) => { - if (!e.target.closest('.metadata-btn') && !e.target.closest('.metadata-popup')) { - this.hidePopup(); - } - }); - - // Close popup on Escape key - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { - this.hidePopup(); - } - }); - } - - showPopup(button, plotName, metadata) { - // Hide any existing popup - this.hidePopup(); - - // Create popup element - const popup = document.createElement('div'); - popup.className = 'metadata-popup'; - popup.innerHTML = this.formatMetadata(plotName, metadata); - - // Position popup relative to button - const rect = button.getBoundingClientRect(); - popup.style.position = 'fixed'; - popup.style.left = rect.left + 'px'; - popup.style.top = (rect.bottom + 5) + 'px'; - popup.style.zIndex = '1000'; - - // Add to DOM - document.body.appendChild(popup); - this.activePopup = popup; - - // Adjust position if popup goes off screen - setTimeout(() => { - const popupRect = popup.getBoundingClientRect(); - - // Adjust horizontal position - if (popupRect.right > window.innerWidth) { - popup.style.left = (rect.right - popupRect.width) + 'px'; - } - - // Adjust vertical position - if (popupRect.bottom > window.innerHeight) { - popup.style.top = (rect.top - popupRect.height - 5) + 'px'; - } - }, 0); - - // Animate in - requestAnimationFrame(() => { - popup.classList.add('show'); - }); - } - - hidePopup() { - if (this.activePopup) { - this.activePopup.classList.remove('show'); - setTimeout(() => { - if (this.activePopup && this.activePopup.parentNode) { - this.activePopup.parentNode.removeChild(this.activePopup); - } - this.activePopup = null; - }, 200); - } - } - - formatMetadata(plotName, metadata) { - if (!metadata || Object.keys(metadata).length === 0) { - return ` - - - `; - } - - let html = ` - - '; - return html; - } - - formatMetadataField(key, value) { - const displayKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); - - let formattedValue; - if (value === null || value === undefined) { - formattedValue = 'null'; - } else if (typeof value === 'object') { - if (Array.isArray(value)) { - if (value.length <= 3) { - formattedValue = value.map(item => ``).join(' '); - } else { - formattedValue = `${value.slice(0, 3).map(item => ``).join(' ')} `; - } - } else { - // Show object as compact JSON for small objects, or just key count for large ones - const keys = Object.keys(value); - if (keys.length <= 3) { - formattedValue = '' + JSON.stringify(value) + ''; - } else { - formattedValue = `Object with ${keys.length} properties`; - } - } - } else { - // Truncate long strings - const str = String(value); - formattedValue = str.length > 50 ? str.substring(0, 47) + '...' : str; - } - - return ` - - `; - } -} - -// Global instance -window.metadataPopup = new MetadataPopup(); - -// Global function for template usage -window.showMetadataPopup = function(button, plotName, metadata) { - window.metadataPopup.showPopup(button, plotName, metadata); -}; diff --git a/assets/js/metadata-section.js b/assets/js/metadata-section.js deleted file mode 100644 index b8f2485..0000000 --- a/assets/js/metadata-section.js +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Simple Metadata Section Toggle - * - * Handles showing/hiding the metadata grid with a simple button - */ - -// Global function to toggle metadata section visibility -function toggleMetadataSection() { - console.log('toggleMetadataSection called'); - - const content = document.getElementById('metadataContent'); - const arrow = document.getElementById('metadataArrow'); - - if (!content) { - console.log('No metadata content found'); - return; - } - - const isVisible = content.style.display !== 'none'; - - if (isVisible) { - // Hide the content - content.style.display = 'none'; - if (arrow) arrow.textContent = '▼'; - console.log('Metadata hidden'); - } else { - // Show the content - content.style.display = 'block'; - if (arrow) arrow.textContent = '▲'; - console.log('Metadata shown'); - - // Trigger MathJax rendering for LaTeX content - if (typeof MathJax !== 'undefined') { - MathJax.typesetPromise([content]).catch(function (err) { - console.log('MathJax typeset failed: ' + err.message); - }); - } - } - - // Save state to localStorage - localStorage.setItem('metadataVisible', (!isVisible).toString()); -} - -// Function to expand long text -function expandText(button) { - const longText = button.previousElementSibling; - const fullText = button.nextElementSibling; - - if (fullText.style.display === 'none') { - longText.style.display = 'none'; - fullText.style.display = 'inline'; - button.textContent = 'Show less'; - } else { - longText.style.display = 'inline'; - fullText.style.display = 'none'; - button.textContent = 'Show more'; - } -} - -// Copy metadata file path to clipboard -async function copyMetadataPath() { - const pathElement = document.getElementById('metadata-file-path'); - const copyBtn = document.querySelector('.copy-path-btn'); - - if (!pathElement || !copyBtn) { - console.log('Path element or copy button not found'); - return; - } - - const path = pathElement.textContent; - console.log('Attempting to copy path:', path); - - try { - // Try using the modern clipboard API - if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(path); - } else { - // Fallback for older browsers - const textArea = document.createElement('textarea'); - textArea.value = path; - textArea.style.position = 'fixed'; - textArea.style.left = '-999999px'; - textArea.style.top = '-999999px'; - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - document.execCommand('copy'); - textArea.remove(); - } - - // Visual feedback - const originalText = copyBtn.innerHTML; - copyBtn.innerHTML = '✅ Copied!'; - copyBtn.classList.add('copied'); - - setTimeout(() => { - copyBtn.innerHTML = originalText; - copyBtn.classList.remove('copied'); - }, 2000); - - console.log('Path copied successfully'); - - } catch (err) { - console.error('Failed to copy path: ', err); - - // Show error feedback - const originalText = copyBtn.innerHTML; - copyBtn.innerHTML = '❌ Failed'; - - setTimeout(() => { - copyBtn.innerHTML = originalText; - }, 2000); - } -} - -// Make functions globally available -window.toggleMetadataSection = toggleMetadataSection; -window.expandText = expandText; -window.copyMetadataPath = copyMetadataPath; - -// Initialize on page load -document.addEventListener('DOMContentLoaded', function() { - console.log('Metadata section script loaded'); - - const content = document.getElementById('metadataContent'); - if (content) { - // Check if user previously had it expanded - const wasVisible = localStorage.getItem('metadataVisible') === 'true'; - - if (wasVisible) { - content.style.display = 'block'; - const arrow = document.getElementById('metadataArrow'); - if (arrow) arrow.textContent = '▲'; - } else { - content.style.display = 'none'; - const arrow = document.getElementById('metadataArrow'); - if (arrow) arrow.textContent = '▼'; - } - - console.log('Metadata section initialized, visible:', wasVisible); - } -}); diff --git a/assets/js/navigation-manager.js b/assets/js/navigation-manager.js deleted file mode 100644 index b9a69d6..0000000 --- a/assets/js/navigation-manager.js +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Navigation functionality - breadcrumbs and folder tree - */ -export class NavigationManager { - /** - * Build breadcrumb navigation based on current path - */ - buildBreadcrumb() { - const currentPath = window.location.pathname; - const pathParts = currentPath.split('/').filter(part => part !== '' && part !== 'index.html'); - const breadcrumb = document.getElementById('breadcrumb'); - - if (!breadcrumb) return; - - if (pathParts.length === 0) { - breadcrumb.innerHTML = '🏠 Root'; - return; - } - - let html = '🏠 Root'; - - for (let i = 0; i < pathParts.length; i++) { - const part = pathParts[i]; - html += '/'; - - if (i === pathParts.length - 1) { - html += `${decodeURIComponent(part)}`; - } else { - const levelsUp = pathParts.length - 1 - i; - const relativePath = '../'.repeat(levelsUp) + 'index.html'; - html += `${decodeURIComponent(part)}`; - } - } - - breadcrumb.innerHTML = html; - } - - /** - * Build and display the folder tree structure - */ - async buildFolderTree() { - const treeContainer = document.getElementById('folderTree'); - const currentPath = window.location.pathname; - - if (!treeContainer) return; - - try { - const tree = await this.buildTreeRecursive(currentPath, 0, currentPath); - treeContainer.innerHTML = tree; - } catch (error) { - console.error('Error building folder tree:', error); - treeContainer.innerHTML = '
❌ Error loading folder tree
'; - } - } - - /** - * Recursively build tree structure for folders with collapsed empty directories - */ - async buildTreeRecursive(path, depth, currentPath, maxDepth = 5) { - if (depth > maxDepth) { - const indent = ' '.repeat(depth); - return `
${indent}└─ ...
`; - } - - try { - const response = await fetch(path); - const htmlContent = await response.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(htmlContent, 'text/html'); - - const subdirs = doc.querySelectorAll('h2 + ul li a'); - const items = doc.querySelectorAll('.grid-item'); - - // Check if this is an empty directory (only one subdirectory, no items) - if (items.length === 0 && subdirs.length === 1) { - const subdir = subdirs[0]; - const href = subdir.getAttribute('href'); - if (href) { - const baseUrl = path.replace(/\/[^\/]*$/, '/'); - const subPath = baseUrl + href; - - const collapsedPath = await this.getCollapsedPath(path, subPath); - return await this.buildCollapsedTreeItem(collapsedPath, depth, currentPath, maxDepth); - } - } - - // Normal directory processing - const indent = ' '.repeat(depth); - const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery'; - const totalItems = items.length + subdirs.length; - const arrow = depth === 0 ? '' : '└─ '; - - let html = ''; - if (path === currentPath) { - html += `
${indent}${arrow}📁 ${folderName} (${totalItems} items)
`; - } else { - html += `
${indent}${arrow}📁 ${folderName} (${totalItems} items)
`; - } - - for (let i = 0; i < subdirs.length; i++) { - const subdir = subdirs[i]; - const href = subdir.getAttribute('href'); - if (href) { - const baseUrl = path.replace(/\/[^\/]*$/, '/'); - const subPath = baseUrl + href; - html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth); - } - } - - return html; - } catch (error) { - const indent = ' '.repeat(depth); - const arrow = depth === 0 ? '' : '└─ '; - const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery'; - return `
${indent}${arrow}📁 ${folderName} (error loading)
`; - } - } - - /** - * Get the collapsed path by following empty directories - */ - async getCollapsedPath(startPath, currentPath) { - const pathSegments = []; - let path = startPath; - - while (true) { - try { - const response = await fetch(path); - const htmlContent = await response.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(htmlContent, 'text/html'); - - const subdirs = doc.querySelectorAll('h2 + ul li a'); - const items = doc.querySelectorAll('.grid-item'); - - const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery'; - pathSegments.push({ name: folderName, path: path }); - - if (items.length > 0 || subdirs.length !== 1) { - break; - } - - const subdir = subdirs[0]; - const href = subdir.getAttribute('href'); - if (href) { - const baseUrl = path.replace(/\/[^\/]*$/, '/'); - path = baseUrl + href; - } else { - break; - } - } catch (error) { - break; - } - } - - return { - segments: pathSegments, - finalPath: path - }; - } - - /** - * Build a collapsed tree item for empty directory chains - */ - async buildCollapsedTreeItem(collapsedPath, depth, currentPath, maxDepth) { - const indent = ' '.repeat(depth); - const arrow = depth === 0 ? '' : '└─ '; - - const displayName = collapsedPath.segments.map(seg => seg.name).join(' / '); - const finalPath = collapsedPath.finalPath; - - let totalItems = 0; - let subdirs = []; - try { - const response = await fetch(finalPath); - const htmlContent = await response.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(htmlContent, 'text/html'); - - const subdirElements = doc.querySelectorAll('h2 + ul li a'); - const items = doc.querySelectorAll('.grid-item'); - totalItems = items.length + subdirElements.length; - - subdirs = Array.from(subdirElements); - } catch (error) { - // Handle error case - } - - let html = ''; - if (finalPath === currentPath) { - html += `
${indent}${arrow}📁 ${displayName} (${totalItems} items)
`; - } else { - html += `
${indent}${arrow}📁 ${displayName} (${totalItems} items)
`; - } - - for (let i = 0; i < subdirs.length; i++) { - const subdir = subdirs[i]; - const href = subdir.getAttribute('href'); - if (href) { - const baseUrl = finalPath.replace(/\/[^\/]*$/, '/'); - const subPath = baseUrl + href; - html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth); - } - } - - return html; - } -} diff --git a/assets/js/recent-plots-manager.js b/assets/js/recent-plots-manager.js deleted file mode 100644 index 29fd3bc..0000000 --- a/assets/js/recent-plots-manager.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Recent plots sidebar management - */ -export class RecentPlotsManager { - constructor(maxRecentPlots = 20) { - this.MAX_RECENT_PLOTS = maxRecentPlots; - this.init(); - } - - init() { - this.updateRecentPlotsDisplay(); - this.trackPlotClicks(); - } - - /** - * Add plot to recent plots list - */ - addToRecentPlots(plotHref) { - const plotName = plotHref.split('/').pop().replace('.pdf', ''); - const pathParts = plotHref.split('/').filter(p => p !== '' && p !== plotName + '.pdf'); - const plotPath = pathParts.join(' / '); - const thumbUrl = plotHref.replace('.pdf', '.png'); - - // Determine the gallery page URL (directory containing the plot) - const plotDir = plotHref.substring(0, plotHref.lastIndexOf('/')); - const galleryUrl = plotDir + '/index.html'; - - const plotInfo = { - name: plotName, - path: plotPath, - href: plotHref, - thumbUrl: thumbUrl, - galleryUrl: galleryUrl, - timestamp: Date.now() - }; - - let recentPlots = JSON.parse(localStorage.getItem('recentPlots') || '[]'); - recentPlots = recentPlots.filter(p => p.href !== plotHref); - recentPlots.unshift(plotInfo); - recentPlots = recentPlots.slice(0, this.MAX_RECENT_PLOTS); - - localStorage.setItem('recentPlots', JSON.stringify(recentPlots)); - this.updateRecentPlotsDisplay(); - } - - /** - * Update recent plots sidebar display - */ - updateRecentPlotsDisplay() { - const sidebarContent = document.getElementById('sidebarContent'); - if (!sidebarContent) return; - - const recentPlots = JSON.parse(localStorage.getItem('recentPlots') || '[]'); - - if (recentPlots.length === 0) { - sidebarContent.innerHTML = ` -
- 📭 No recent plots yet
- Open some plots to see them here -
- `; - return; - } - - let html = ''; - recentPlots.forEach(plot => { - html += ` -
- ${plot.name} -
-
${plot.name}
-
📍 ${plot.path}
-
-
- `; - }); - - sidebarContent.innerHTML = html; - } - - /** - * Open recent plot gallery page and highlight thumbnail - */ - openRecentPlot(galleryUrl, plotName) { - // Navigate to gallery page with plot highlight parameter - const url = new URL(galleryUrl, window.location.origin); - url.searchParams.set('highlight', plotName); - window.location.href = url.toString(); - this.toggleSidebar(); - } - - /** - * Toggle recent plots sidebar - */ - toggleSidebar() { - const sidebar = document.getElementById('sidebar'); - const overlay = document.getElementById('sidebarOverlay'); - - if (sidebar) sidebar.classList.toggle('open'); - if (overlay) overlay.classList.toggle('open'); - } - - /** - * Track clicks on plot links - */ - trackPlotClicks() { - document.addEventListener('click', (e) => { - const link = e.target.closest('a[href$=".pdf"]'); - if (link) { - this.addToRecentPlots(link.href); - } - }); - } -} diff --git a/assets/js/search-manager.js b/assets/js/search-manager.js deleted file mode 100644 index 20eff08..0000000 --- a/assets/js/search-manager.js +++ /dev/null @@ -1,217 +0,0 @@ -/** - * Search functionality - */ -export class SearchManager { - constructor(debounceMs = 300) { - this.searchTimeout = null; - this.SEARCH_DEBOUNCE_MS = debounceMs; - this.init(); - } - - /** - * Initialize search functionality with debouncing - */ - init() { - const searchBox = document.getElementById('searchBox'); - const searchResults = document.getElementById('searchResults'); - - if (!searchBox || !searchResults) return; - - searchBox.addEventListener('input', (e) => { - clearTimeout(this.searchTimeout); - const query = e.target.value.trim(); - - if (query.length === 0) { - searchResults.style.display = 'none'; - return; - } - - this.searchTimeout = setTimeout(() => { - this.performSearch(query); - }, this.SEARCH_DEBOUNCE_MS); - }); - - document.addEventListener('click', (e) => { - if (!searchBox.contains(e.target) && !searchResults.contains(e.target)) { - searchResults.style.display = 'none'; - } - }); - } - - /** - * Perform search across plot names - */ - async performSearch(query) { - const searchResults = document.getElementById('searchResults'); - if (!searchResults) return; - - searchResults.innerHTML = '
🔍 Searching...
'; - searchResults.style.display = 'block'; - - try { - const results = await this.searchPlots(query); - this.displaySearchResults(results, query); - } catch (error) { - console.error('Search error:', error); - searchResults.innerHTML = '
❌ Search failed
'; - } - } - - /** - * Search for plots matching the query - */ - async searchPlots(query) { - const results = []; - const visited = new Set(); - const lowerQuery = query.toLowerCase(); - - await this.searchInPage(window.location.pathname, lowerQuery, results, visited); - await this.searchRecursive(window.location.pathname, lowerQuery, results, visited, 0, 5); - - return results.slice(0, 20); - } - - /** - * Search for plots in a specific page - */ - async searchInPage(path, query, results, visited, maxResults = 50) { - if (visited.has(path) || results.length >= maxResults) return; - visited.add(path); - - try { - const response = await fetch(path); - const html = await response.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - - const items = doc.querySelectorAll('.grid-item'); - items.forEach(item => { - const nameElement = item.querySelector('.plot-name'); - const imgElement = item.querySelector('img'); - const linkElement = item.querySelector('a'); - - if (nameElement && imgElement && linkElement) { - const name = nameElement.textContent.toLowerCase(); - if (name.includes(query)) { - results.push({ - name: nameElement.textContent, - path: path, - href: linkElement.href, - imgSrc: imgElement.src, - relevance: this.calculateRelevance(name, query) - }); - } - } - }); - } catch (error) { - console.error('Error searching in', path, error); - } - } - - /** - * Recursively search in subdirectories - */ - async searchRecursive(path, query, results, visited, depth, maxDepth) { - if (depth >= maxDepth || results.length >= 50) return; - - try { - const response = await fetch(path); - const html = await response.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - - const subdirs = doc.querySelectorAll('h2 + ul li a'); - for (const subdir of subdirs) { - const href = subdir.getAttribute('href'); - if (href) { - const baseUrl = path.replace(/\/[^\/]*$/, '/'); - const subPath = baseUrl + href; - await this.searchInPage(subPath, query, results, visited); - await this.searchRecursive(subPath, query, results, visited, depth + 1, maxDepth); - } - } - } catch (error) { - console.error('Error in recursive search:', error); - } - } - - /** - * Calculate search relevance score - */ - calculateRelevance(text, query) { - const exactMatch = text === query; - const startsWith = text.startsWith(query); - const wordMatch = text.split(/\s+/).some(word => word.startsWith(query)); - - if (exactMatch) return 100; - if (startsWith) return 80; - if (wordMatch) return 60; - return 40; - } - - /** - * Display search results with highlighting - */ - displaySearchResults(results, query) { - const searchResults = document.getElementById('searchResults'); - if (!searchResults) return; - - if (results.length === 0) { - searchResults.innerHTML = '
📭 No plots found
'; - return; - } - - results.sort((a, b) => b.relevance - a.relevance); - - let html = ''; - results.forEach(result => { - const highlightedName = this.highlightText(result.name, query); - const relativePath = this.getRelativePath(result.path); - - html += ` -
-
- -
-
- ${highlightedName} -
-
- 📍 ${relativePath} -
-
-
-
- `; - }); - - searchResults.innerHTML = html; - } - - /** - * Highlight search query in text - */ - highlightText(text, query) { - const regex = new RegExp(`(${query})`, 'gi'); - return text.replace(regex, '$1'); - } - - /** - * Get relative path for display - */ - getRelativePath(fullPath) { - const parts = fullPath.split('/').filter(p => p !== '' && p !== 'index.html'); - return parts.length > 0 ? parts.join(' / ') : 'Root'; - } - - /** - * Open search result and track it - */ - openSearchResult(href) { - if (window.recentPlotsManager) { - window.recentPlotsManager.addToRecentPlots(href); - } - window.open(href, '_blank'); - document.getElementById('searchResults').style.display = 'none'; - } -} diff --git a/assets/js/sort-manager.js b/assets/js/sort-manager.js deleted file mode 100644 index 8ec9a85..0000000 --- a/assets/js/sort-manager.js +++ /dev/null @@ -1,197 +0,0 @@ -/** - * Sort Manager - handles sorting of plot items by name and creation time - */ -export class SortManager { - constructor() { - this.currentSort = 'name'; - this.currentOrder = 'asc'; - this.init(); - } - - /** - * Initialize sort controls - */ - init() { - // Use setTimeout to ensure DOM is ready - setTimeout(() => { - this.setupSortButtons(); - this.loadSavedSort(); - }, 100); - } - - /** - * Setup sort button event listeners - */ - setupSortButtons() { - const sortButtons = document.querySelectorAll('.sort-btn'); - const orderButton = document.querySelector('.sort-order-btn'); - - if (sortButtons.length === 0) { - setTimeout(() => this.setupSortButtons(), 500); - return; - } - - sortButtons.forEach((button) => { - const sortType = button.getAttribute('data-sort'); - - button.addEventListener('click', (e) => { - e.preventDefault(); - this.setSortType(sortType); - }); - }); - - if (orderButton) { - orderButton.addEventListener('click', (e) => { - e.preventDefault(); - this.toggleSortOrder(); - }); - } - - // Initialize button states - this.updateSortButtons(); - this.updateOrderButton(); - } - - /** - * Set the sort type (name or time) - */ - setSortType(sortType) { - if (sortType === this.currentSort) return; - - this.currentSort = sortType; - this.updateSortButtons(); - this.sortPlots(); - this.saveSortPreference(); - } - - /** - * Toggle sort order between ascending and descending - */ - toggleSortOrder() { - this.currentOrder = this.currentOrder === 'asc' ? 'desc' : 'asc'; - this.updateOrderButton(); - this.sortPlots(); - this.saveSortPreference(); - } - - /** - * Update visual state of sort buttons - */ - updateSortButtons() { - const sortButtons = document.querySelectorAll('.sort-btn'); - - sortButtons.forEach(btn => { - if (btn.getAttribute('data-sort') === this.currentSort) { - btn.classList.add('active'); - } else { - btn.classList.remove('active'); - } - }); - } - - /** - * Update visual state of order button - */ - updateOrderButton() { - const orderButton = document.querySelector('.sort-order-btn'); - if (orderButton) { - orderButton.textContent = this.currentOrder === 'asc' ? '↑' : '↓'; - orderButton.setAttribute('data-order', this.currentOrder); - orderButton.title = `Sort Order: ${this.currentOrder === 'asc' ? 'Ascending' : 'Descending'}`; - } - } - - /** - * Sort the plot items - */ - sortPlots() { - const plotContainer = document.getElementById('plotContainer'); - if (!plotContainer) return; - - const plotItems = Array.from(plotContainer.children); - - plotItems.sort((a, b) => { - let valueA, valueB; - - if (this.currentSort === 'name') { - valueA = a.getAttribute('data-name') || ''; - valueB = b.getAttribute('data-name') || ''; - - // Natural sort for better number handling - const result = valueA.localeCompare(valueB, undefined, { - numeric: true, - sensitivity: 'base' - }); - return this.currentOrder === 'asc' ? result : -result; - } else if (this.currentSort === 'time') { - valueA = parseInt(a.getAttribute('data-time') || '0'); - valueB = parseInt(b.getAttribute('data-time') || '0'); - - const result = valueA - valueB; - return this.currentOrder === 'asc' ? result : -result; - } - - return 0; - }); - - // Re-append sorted items - plotItems.forEach(item => { - plotContainer.appendChild(item); - }); - } - - /** - * Save sort preferences to localStorage - */ - saveSortPreference() { - try { - localStorage.setItem('gallery-sort-type', this.currentSort); - localStorage.setItem('gallery-sort-order', this.currentOrder); - } catch (e) { - // Ignore localStorage errors - } - } - - /** - * Load saved sort preferences - */ - loadSavedSort() { - try { - const savedSort = localStorage.getItem('gallery-sort-type'); - const savedOrder = localStorage.getItem('gallery-sort-order'); - - if (savedSort && ['name', 'time'].includes(savedSort)) { - this.currentSort = savedSort; - } - - if (savedOrder && ['asc', 'desc'].includes(savedOrder)) { - this.currentOrder = savedOrder; - } - - this.updateSortButtons(); - this.updateOrderButton(); - - // Sort immediately if there are plots - setTimeout(() => this.sortPlots(), 100); - } catch (e) { - // Ignore localStorage errors, use defaults - } - } - - /** - * Get current sort settings - */ - getCurrentSort() { - return { - type: this.currentSort, - order: this.currentOrder - }; - } - - /** - * Refresh sorting (call this when plot content changes) - */ - refresh() { - this.sortPlots(); - } -} diff --git a/assets/js/stats-manager.js b/assets/js/stats-manager.js deleted file mode 100644 index 9ad8df5..0000000 --- a/assets/js/stats-manager.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Statistics manager for gallery display - */ -export class StatsManager { - constructor() { - this.updateGalleryStats(); - } - - /** - * Update gallery statistics display - */ - updateGalleryStats() { - // Use stats passed from Python backend if available - // Otherwise fallback to DOM counting - const gridItems = document.querySelectorAll('.grid-item'); - const subdirLinks = document.querySelectorAll('a[href$="/index.html"]'); - - const fileCountEl = document.getElementById('fileCount'); - const folderCountEl = document.getElementById('folderCount'); - const totalSizeEl = document.getElementById('totalSize'); - const lastUpdatedEl = document.getElementById('lastUpdated'); - - if (fileCountEl) fileCountEl.textContent = gridItems.length; - if (folderCountEl) folderCountEl.textContent = subdirLinks.length; - if (totalSizeEl) totalSizeEl.textContent = 'Unknown'; - - // Set last updated time - const now = new Date(); - const timeStr = now.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit' - }); - if (lastUpdatedEl) lastUpdatedEl.textContent = timeStr; - } - - /** - * Update stats with backend data - */ - updateWithBackendStats(stats) { - const fileCountEl = document.getElementById('fileCount'); - const folderCountEl = document.getElementById('folderCount'); - const totalSizeEl = document.getElementById('totalSize'); - - if (fileCountEl && stats.file_count !== undefined) { - fileCountEl.textContent = stats.file_count; - } - if (folderCountEl && stats.folder_count !== undefined) { - folderCountEl.textContent = stats.folder_count; - } - if (totalSizeEl && stats.total_size !== undefined) { - totalSizeEl.textContent = stats.total_size; - } - } -} diff --git a/assets/js/theme-manager.js b/assets/js/theme-manager.js deleted file mode 100644 index f23affc..0000000 --- a/assets/js/theme-manager.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Theme management functionality - */ -export class ThemeManager { - constructor() { - this.init(); - } - - /** - * Initialize theme system and load saved preference - */ - init() { - const savedTheme = localStorage.getItem('theme'); - const html = document.documentElement; - const themeToggle = document.getElementById('themeToggle'); - - if (savedTheme === 'light') { - html.setAttribute('data-theme', 'light'); - if (themeToggle) themeToggle.textContent = '🌙'; - } else { - html.removeAttribute('data-theme'); - if (themeToggle) themeToggle.textContent = '☀️'; - } - } - - /** - * Toggle between light and dark themes - */ - toggle() { - const html = document.documentElement; - const themeToggle = document.getElementById('themeToggle'); - - if (html.getAttribute('data-theme') === 'light') { - html.removeAttribute('data-theme'); - if (themeToggle) themeToggle.textContent = '☀️'; - localStorage.setItem('theme', 'dark'); - } else { - html.setAttribute('data-theme', 'light'); - if (themeToggle) themeToggle.textContent = '🌙'; - localStorage.setItem('theme', 'light'); - } - } -} diff --git a/assets/js/utils.js b/assets/js/utils.js deleted file mode 100644 index 3c5b432..0000000 --- a/assets/js/utils.js +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Utility functions and helpers - */ -export class Utils { - /** - * Format file size in human readable format - */ - static formatFileSize(bytes) { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; - } - - /** - * Handle thumbnail highlighting from URL parameters - */ - static handleThumbnailHighlight() { - const urlParams = new URLSearchParams(window.location.search); - const highlightPlot = urlParams.get('highlight'); - - if (highlightPlot) { - // Find and highlight the thumbnail - const gridItems = document.querySelectorAll('.grid-item'); - gridItems.forEach(item => { - const plotName = item.querySelector('.plot-name'); - if (plotName && plotName.textContent.trim() === highlightPlot) { - item.classList.add('highlighted'); - // Scroll to the highlighted item - setTimeout(() => { - item.scrollIntoView({ - behavior: 'smooth', - block: 'center' - }); - }, 100); - // Remove highlight after animation - setTimeout(() => { - item.classList.remove('highlighted'); - }, 3000); - } - }); - - // Clean up URL - const newUrl = new URL(window.location); - newUrl.searchParams.delete('highlight'); - window.history.replaceState({}, document.title, newUrl.toString()); - } - } - - /** - * Calculate approximate total size of displayed files - */ - static async calculateApproximateSize() { - const images = document.querySelectorAll('.grid-item img'); - let totalSize = 0; - let loadedCount = 0; - - const sizeElement = document.getElementById('totalSize'); - if (!sizeElement) return; - - sizeElement.textContent = 'Loading...'; - - // Estimate size based on a sample of images - const sampleSize = Math.min(images.length, 5); - const sampleImages = Array.from(images).slice(0, sampleSize); - - if (sampleImages.length === 0) { - sizeElement.textContent = '0 KB'; - return; - } - - // Calculate average size from sample - for (const img of sampleImages) { - try { - const response = await fetch(img.src, { method: 'HEAD' }); - const size = parseInt(response.headers.get('content-length') || '0'); - if (size > 0) { - totalSize += size; - loadedCount++; - } - } catch (e) { - // Fallback: estimate 100KB per image - totalSize += 102400; - loadedCount++; - } - } - - if (loadedCount > 0) { - const averageSize = totalSize / loadedCount; - const estimatedTotal = averageSize * images.length; - sizeElement.textContent = Utils.formatFileSize(estimatedTotal); - } else { - sizeElement.textContent = 'Unknown'; - } - } - - /** - * Toggle keyboard shortcuts help display - */ - static toggleShortcutsHelp() { - const help = document.getElementById('shortcutsHelp'); - if (help) { - help.style.display = help.style.display === 'block' ? 'none' : 'block'; - } - } -} diff --git a/assets/js/view-manager.js b/assets/js/view-manager.js deleted file mode 100644 index 0d51208..0000000 --- a/assets/js/view-manager.js +++ /dev/null @@ -1,192 +0,0 @@ -/** - * View Controls Manager - handles switching between grid, list-large, and list-compact views - */ -export class ViewManager { - constructor() { - this.currentView = 'grid'; - this.init(); - } - - /** - * Initialize view controls - */ - init() { - this.setupViewButtons(); - this.loadSavedView(); - this.updateControlsVisibility(); - } - - /** - * Update visibility of view controls based on plot content - */ - updateControlsVisibility() { - const plotContainer = document.getElementById('plotContainer'); - const controlsContainer = document.querySelector('.controls-container'); - - if (!plotContainer || !controlsContainer) return; - - const hasPlots = plotContainer.children.length > 0; - controlsContainer.style.display = hasPlots ? 'flex' : 'none'; - } - - /** - * Setup view toggle buttons - */ - setupViewButtons() { - const viewButtons = document.querySelectorAll('.view-btn'); - - viewButtons.forEach(button => { - button.addEventListener('click', (e) => { - const newView = button.getAttribute('data-view'); - this.switchView(newView); - }); - }); - } - - /** - * Switch to a different view mode - */ - switchView(viewMode) { - if (viewMode === this.currentView) return; - - const plotContainer = document.getElementById('plotContainer'); - const viewButtons = document.querySelectorAll('.view-btn'); - - if (!plotContainer) return; - - // Remove current view class - plotContainer.classList.remove( - 'grid-view', - 'list-large-view', - 'list-compact-view' - ); - - // Add new view class - switch (viewMode) { - case 'grid': - plotContainer.classList.add('grid-view'); - break; - case 'list-large': - plotContainer.classList.add('list-large-view'); - break; - case 'list-compact': - plotContainer.classList.add('list-compact-view'); - break; - default: - plotContainer.classList.add('grid-view'); - viewMode = 'grid'; - } - - // Update button states - viewButtons.forEach(btn => { - if (btn.getAttribute('data-view') === viewMode) { - btn.classList.add('active'); - } else { - btn.classList.remove('active'); - } - }); - - // Save the preference - this.currentView = viewMode; - this.saveViewPreference(viewMode); - - // Trigger any necessary layout updates - this.onViewChanged(viewMode); - } - - /** - * Save view preference to localStorage - */ - saveViewPreference(viewMode) { - try { - localStorage.setItem('gallery-view-mode', viewMode); - } catch (e) { - // Ignore localStorage errors - } - } - - /** - * Load saved view preference - */ - loadSavedView() { - try { - const savedView = localStorage.getItem('gallery-view-mode'); - if (savedView && ['grid', 'list-large', 'list-compact'].includes(savedView)) { - this.switchView(savedView); - } - } catch (e) { - // Ignore localStorage errors, use default - } - } - - /** - * Handle view change events - can be extended for additional functionality - */ - onViewChanged(viewMode) { - // Dispatch custom event for other components that might need to know - const event = new CustomEvent('viewChanged', { - detail: { viewMode } - }); - document.dispatchEvent(event); - - // Update any other UI elements that depend on view mode - this.updateUIForView(viewMode); - } - - /** - * Update UI elements based on current view - */ - updateUIForView(viewMode) { - // You can add view-specific UI updates here - // For example, adjusting search result highlighting, etc. - - // Update any tooltips or help text - const viewButtons = document.querySelectorAll('.view-btn'); - viewButtons.forEach(btn => { - const btnView = btn.getAttribute('data-view'); - if (btnView === viewMode) { - btn.style.transform = 'scale(1.05)'; - } else { - btn.style.transform = 'scale(1)'; - } - }); - } - - /** - * Get current view mode - */ - getCurrentView() { - return this.currentView; - } - - /** - * Refresh controls visibility (call this when gallery content changes) - */ - refresh() { - this.updateControlsVisibility(); - } - - /** - * Check if current view is grid mode - */ - isGridView() { - return this.currentView === 'grid'; - } - - /** - * Check if current view is list mode (either variant) - */ - isListView() { - return this.currentView === 'list-large' || this.currentView === 'list-compact'; - } - - /** - * Cycle through view modes (useful for keyboard shortcuts) - */ - cycleView() { - const views = ['grid', 'list-large', 'list-compact']; - const currentIndex = views.indexOf(this.currentView); - const nextIndex = (currentIndex + 1) % views.length; - this.switchView(views[nextIndex]); - } -} diff --git a/config.lbogner.yaml b/config.lbogner.yaml index ca41126..52243b3 100644 --- a/config.lbogner.yaml +++ b/config.lbogner.yaml @@ -14,7 +14,6 @@ paths: gallery: plot_root: "gallery" png_dpi: 400 - backup_folder: "" ui: max_recent_plots: 20 diff --git a/config.yaml b/config.yaml index b30ed9c..cd990fd 100644 --- a/config.yaml +++ b/config.yaml @@ -17,9 +17,6 @@ gallery: # PNG conversion quality png_dpi: 400 - # Backup folder (leave empty to disable) - backup_folder: "" - # UI Settings ui: # Maximum number of recent plots to track diff --git a/docs/COVERAGE_TESTING.md b/docs/COVERAGE_TESTING.md deleted file mode 100644 index e834301..0000000 --- a/docs/COVERAGE_TESTING.md +++ /dev/null @@ -1,275 +0,0 @@ -# Automated Coverage Testing Documentation - -## Overview - -This repository now includes automated code coverage testing using the `coverage.py` package. Coverage testing helps ensure that your tests adequately exercise your codebase and identifies untested code paths. - -## 🚀 Quick Start - -### Container-based Coverage (Recommended) -```bash -# Build and test with coverage in container -./tests/test_container.sh - -# Or run coverage directly in container -apptainer exec gallery-generator.sif python3 /src/tests/run_coverage.py -``` - -### Local Coverage Testing -```bash -# Run coverage tests locally -./tests/run_coverage_local.sh - -# Or manually -pip install coverage -coverage run -m unittest tests.test_container -coverage report -coverage html -``` - -## 📁 Coverage Files - -### Core Coverage Files -- **`.coveragerc`** - Coverage configuration file -- **`tests/run_coverage.py`** - Automated coverage script for containers -- **`tests/run_coverage_local.sh`** - Local coverage testing script - -### Generated Reports -- **`coverage.xml`** - XML format for CI/CD integration -- **`coverage_html_report/`** - Interactive HTML reports -- **`.coverage`** - Coverage data file - -## 🔧 Configuration - -### Coverage Settings (`.coveragerc`) -```ini -[run] -source = . -omit = - tests/* # Exclude test files - __pycache__/* # Exclude cache - assets/* # Exclude static assets - docs/* # Exclude documentation - templates/* # Exclude templates - -[report] -precision = 2 # 2 decimal places -show_missing = True # Show missing line numbers -skip_covered = False # Show all files - -[html] -directory = coverage_html_report -title = Gallery Generator Coverage Report -``` - -### Singularity Container Integration -The coverage package is automatically installed in the container: -```bash -pip install --no-cache-dir jinja2 pyyaml coverage -``` - -## 📊 Coverage Reports - -### Console Report -Shows coverage percentage and missing lines: -``` -Name Stmts Miss Cover Missing ------------------------------------------------------ -generate_gallery.py 190 45 76.32% 156-167, 234-245 -orchestration/config.py 45 8 82.22% 78-82 -orchestration/logger.py 67 12 82.09% 45-48, 89-94 ------------------------------------------------------ -TOTAL 302 65 78.48% -``` - -### HTML Report -Interactive report with: -- Line-by-line coverage highlighting -- Branch coverage details -- Sortable file listings -- Coverage trends - -### XML Report -Machine-readable format for CI/CD: -- GitLab CI coverage visualization -- External tool integration -- Coverage badges - -## 🎯 Coverage Targets - -### Current Thresholds -- **Minimum Target**: 80% overall coverage -- **Warning Level**: Below 70% coverage -- **Exclusions**: Test files, static assets, documentation - -### Best Practices -- **Focus on Core Logic**: Prioritize business logic coverage -- **Test Edge Cases**: Include error handling and boundary conditions -- **Regular Monitoring**: Run coverage with every commit -- **Incremental Improvement**: Gradually increase coverage over time - -## 🔄 CI/CD Integration - -### GitLab CI Pipeline -The coverage testing is integrated into the GitLab CI pipeline: - -```yaml -test:coverage: - stage: test - script: - - apptainer exec $CONTAINER_IMAGE python3 /src/tests/run_coverage.py - coverage: '/TOTAL.+?(\d+\.\d+)%/' - artifacts: - reports: - coverage_report: - coverage_format: cobertura - path: coverage.xml -``` - -### Features -- **Automatic Reports**: Coverage reports in merge requests -- **Badge Integration**: Coverage badges in README -- **Trend Tracking**: Historical coverage data -- **Failure Thresholds**: Fail builds below minimum coverage - -## 🛠️ Advanced Usage - -### Custom Coverage Runs -```bash -# Test specific modules -coverage run --source=orchestration -m unittest tests.test_metadata - -# Include/exclude patterns -coverage run --omit="*/tests/*" -m unittest discover - -# Branch coverage (more detailed) -coverage run --branch -m unittest tests.test_container -``` - -### Coverage Analysis -```bash -# Show missing lines -coverage report --show-missing - -# Generate detailed HTML -coverage html --show-contexts - -# Export data -coverage json -coverage xml -``` - -### Integration with IDEs -- **VS Code**: Coverage Gutters extension -- **PyCharm**: Built-in coverage runner -- **Vim**: Coverage highlighting plugins - -## 📈 Coverage Metrics - -### What Coverage Measures -- **Statement Coverage**: Lines of code executed -- **Branch Coverage**: Decision paths taken -- **Function Coverage**: Functions called -- **Class Coverage**: Classes instantiated - -### What Coverage Doesn't Measure -- **Code Quality**: Coverage ≠ good tests -- **Logic Correctness**: 100% coverage ≠ bug-free -- **Performance**: Execution speed not measured -- **Security**: Vulnerabilities not detected - -## 🧪 Testing Strategy - -### Container Test Suite Coverage -Current test files and their focus: - -#### `tests/test_container.py` -- **Environment validation** - Container setup -- **Utility functions** - Helper functions -- **Metadata system** - YAML processing -- **PDF processing** - ImageMagick integration -- **Gallery generation** - End-to-end workflow - -#### `tests/test_build_container.py` -- **Container building** - Singularity build process -- **Dependency validation** - Package installation -- **Application functionality** - Script execution - -### Coverage Gaps Analysis -Use `tests/test_coverage.py` to analyze: -- Missing function coverage -- Untested code paths -- Critical functionality gaps -- Integration test needs - -## 🚨 Troubleshooting - -### Common Issues - -#### No Coverage Data -```bash -# Ensure coverage is running tests -coverage run --debug=trace -m unittest tests.test_container -``` - -#### Import Errors -```bash -# Check PYTHONPATH -export PYTHONPATH=/src:$PYTHONPATH -``` - -#### Permission Issues -```bash -# Container write permissions -apptainer exec --writable-tmpfs container.sif python3 tests/run_coverage.py -``` - -### Debug Commands -```bash -# Check coverage configuration -coverage debug config - -# Verify data collection -coverage debug data - -# Test discovery -coverage debug sys -``` - -## 📚 References - -- **Coverage.py Documentation**: https://coverage.readthedocs.io/ -- **GitLab CI Coverage**: https://docs.gitlab.com/ee/ci/testing/code_coverage.html -- **Testing Best Practices**: Python Testing 101 -- **Container Testing**: Singularity/Apptainer Documentation - -## 🔄 Maintenance - -### Regular Tasks -- **Weekly**: Review coverage reports -- **Monthly**: Update coverage targets -- **Release**: Ensure minimum coverage met -- **Quarterly**: Review exclusion patterns - -### Cleanup -```bash -# Remove coverage files -./tests/cleanup.sh - -# Manual cleanup -rm -f .coverage coverage.xml -rm -rf coverage_html_report/ -``` - -### Updates -```bash -# Update coverage package -pip install --upgrade coverage - -# Update container -apptainer build --force container.sif Singularity.def -``` - ---- - -*This automated coverage system provides comprehensive testing insights while maintaining the containerized, dependency-free approach of the gallery generator project.* diff --git a/docs/SORT_IMPLEMENTATION.md b/docs/SORT_IMPLEMENTATION.md deleted file mode 100644 index b0dae4a..0000000 --- a/docs/SORT_IMPLEMENTATION.md +++ /dev/null @@ -1,136 +0,0 @@ -# Gallery Sort Functionality Implementation Guide - -## Overview -This guide explains how to integrate the new sorting functionality that allows users to sort plots by name and creation time. - -## Frontend Implementation (Complete ✅) - -The frontend implementation is complete and includes: - -### 1. Sort Controls UI -- **Name/Time buttons**: Toggle between sorting by filename and creation time -- **Order button**: Toggle between ascending (↑) and descending (↓) order -- **Positioned**: Left side of the controls container, next to view toggle buttons -- **Responsive**: Adapts to mobile layouts - -### 2. Keyboard Shortcuts -- `Ctrl+N`: Sort by name -- `Ctrl+M`: Sort by time (modification/creation time) -- `Ctrl+O`: Toggle sort order (ascending/descending) - -### 3. Persistence -- Sort preferences are saved to localStorage -- Settings persist across page reloads and navigation - -### 4. Tile Sizing -- Grid view now shows ~6 plots per row on desktop (240px minimum width) -- Responsive design maintains usability on mobile devices - -## Backend Integration (Required) - -To enable time-based sorting, you need to modify your Python gallery generation code: - -### 1. Add Creation Time to Plot Items - -```python -from pathlib import Path - -def add_creation_time_to_items(items, base_path): - """Add creation time to plot items for sorting functionality.""" - for item in items: - try: - # Get creation time from PNG or PDF file - png_path = None - pdf_path = None - - if 'png_href' in item: - png_rel_path = item['png_href'].replace('../', '').replace('./', '') - png_path = Path(base_path) / png_rel_path - - if 'pdf_href' in item: - pdf_rel_path = item['pdf_href'].replace('../', '').replace('./', '') - pdf_path = Path(base_path) / pdf_rel_path - - # Use PNG creation time if available, otherwise PDF - creation_time = 0 - if png_path and png_path.exists(): - creation_time = int(png_path.stat().st_ctime) - elif pdf_path and pdf_path.exists(): - creation_time = int(pdf_path.stat().st_ctime) - - item['creation_time'] = creation_time - - except Exception as e: - print(f"Warning: Could not get creation time for {item.get('name', 'unknown')}: {e}") - item['creation_time'] = 0 - - return items -``` - -### 2. Integrate into Your Gallery Generation - -In your existing gallery generation code, call this function before rendering the template: - -```python -# Your existing code that creates the items list -items = generate_plot_items() # Your existing function - -# Add creation times -items = add_creation_time_to_items(items, gallery_base_path) - -# Pass to template -template.render(items=items, ...) -``` - -### 3. Template Data Structure - -The template now expects each item to have a `creation_time` field: - -```python -item = { - 'name': 'plot_name.png', - 'png_href': './plot_name.png', - 'pdf_href': './plot_name.pdf', - 'creation_time': 1642723200 # Unix timestamp -} -``` - -## File Locations - -### Frontend Files (Ready to use) -- `templates/gallery.html` - Updated with sort controls and data attributes -- `assets/css/view-controls.css` - Styling for sort and view controls -- `assets/css/view-override.css` - Grid layout with larger tiles -- `assets/js/sort-manager.js` - Sort functionality implementation -- `assets/js/gallery-app.js` - Integration of SortManager -- `assets/js/keyboard-manager.js` - Keyboard shortcuts for sorting - -### Backend Integration -- `python/add_creation_time.py` - Example implementation for adding creation times - -## Features Summary - -### ✅ Completed Features -1. **Larger Grid Tiles**: ~6 plots per row instead of 8 -2. **Sort Controls**: Name and time sorting with visual feedback -3. **Sort Order Toggle**: Ascending/descending with visual indicator -4. **Keyboard Shortcuts**: Quick access to all sort functions -5. **Persistence**: Settings saved across sessions -6. **Responsive Design**: Works on all screen sizes -7. **Template Integration**: Data attributes ready for backend - -### 🔄 Next Steps (Backend Integration) -1. Modify your Python gallery generation code to include `creation_time` -2. Use the provided `add_creation_time_to_items()` function -3. Test with real plot files to ensure timestamps are correct - -## Testing - -After backend integration: -1. Navigate to a gallery with multiple plots -2. Click the sort buttons to verify functionality -3. Use keyboard shortcuts to test responsiveness -4. Check that sort order toggles correctly -5. Verify settings persist after page reload - -The frontend is fully functional and will work immediately once the backend provides the `creation_time` data. diff --git a/gallery/assets/css/export.css b/gallery/assets/css/export.css deleted file mode 100644 index 84d7b5f..0000000 --- a/gallery/assets/css/export.css +++ /dev/null @@ -1,440 +0,0 @@ -/* ======================================== - EXPORT FUNCTIONALITY STYLES - ======================================== */ - -/* Selection mode styles */ -.selection-mode .grid-item { - cursor: pointer; - transition: all 0.2s ease; -} - -.selection-mode .grid-item:hover { - transform: scale(1.02); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); -} - -/* Selection overlay */ -.selection-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.3); - display: none; - justify-content: center; - align-items: center; - border-radius: 8px; - z-index: 5; -} - -.selection-mode .selection-overlay { - display: flex; -} - -.selection-checkbox { - background: var(--card-background); - border: 2px solid var(--border-color); - border-radius: 50%; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - font-size: 18px; - transition: all 0.2s ease; -} - -.grid-item.selected .selection-checkbox { - background: var(--primary-color, #007bff); - border-color: var(--primary-color, #007bff); - color: white; -} - -.checkbox-icon { - line-height: 1; -} - -/* Selection counter */ -.selection-counter { - position: fixed; - bottom: 100px; - right: 20px; - background: var(--card-background); - border: 1px solid var(--border-color); - border-radius: 20px; - padding: 8px 16px; - font-size: 14px; - font-weight: 500; - color: var(--text-color); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - z-index: 999; -} - -/* Export button styles */ -.export-btn { - background: #28a745 !important; -} - -.export-btn:hover { - background: #218838 !important; -} - -.export-btn:disabled { - background: #6c757d !important; - cursor: not-allowed; -} - -/* Export messages */ -.export-message { - position: fixed; - top: 20px; - right: 20px; - padding: 12px 24px; - border-radius: 6px; - font-weight: 500; - z-index: 1001; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - animation: slideIn 0.3s ease; -} - -.export-message-info { - background: #d1ecf1; - color: #0c5460; - border: 1px solid #bee5eb; -} - -.export-message-success { - background: #d4edda; - color: #155724; - border: 1px solid #c3e6cb; -} - -.export-message-warning { - background: #fff3cd; - color: #856404; - border: 1px solid #ffeaa7; -} - -.export-message-error { - background: #f8d7da; - color: #721c24; - border: 1px solid #f5c6cb; -} - -@keyframes slideIn { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -/* Selection mode indicator */ -.selection-mode::before { - content: "Selection Mode - Click plots to select them"; - position: fixed; - top: 0; - left: 0; - right: 0; - background: var(--primary-color, #007bff); - color: white; - text-align: center; - padding: 8px; - font-size: 14px; - font-weight: 500; - z-index: 1000; -} - -/* Adjust main content when in selection mode */ -.selection-mode { - padding-top: 40px; -} - -/* Export instructions overlay */ -.export-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1002; -} - -.export-instructions { - background: var(--card-background); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 24px; - max-width: 600px; - max-height: 80vh; - overflow-y: auto; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); -} - -.export-instructions h3 { - margin: 0 0 16px 0; - color: var(--text-color); -} - -.export-instructions p { - margin: 0 0 16px 0; - color: var(--text-color); -} - -.export-data { - margin: 16px 0; -} - -.export-data textarea { - width: 100%; - height: 200px; - font-family: 'Courier New', monospace; - font-size: 12px; - border: 1px solid var(--border-color); - border-radius: 4px; - padding: 8px; - background: var(--background-color); - color: var(--text-color); - resize: vertical; -} - -.export-commands { - margin: 16px 0; - padding: 12px; - background: var(--header-background); - border-radius: 4px; - border: 1px solid var(--border-color); -} - -.export-command-container { - margin: 20px 0; - border: 1px solid var(--border-color); - border-radius: 8px; - overflow: hidden; -} - -.export-command { - background: var(--header-background); - padding: 16px; - border-bottom: 1px solid var(--border-color); -} - -.export-command code { - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 13px; - line-height: 1.4; - color: var(--text-color); - word-break: break-all; - display: block; - background: none; - border: none; - padding: 0; - margin: 0; -} - -.export-actions { - display: flex; - gap: 8px; - padding: 12px 16px; - background: var(--card-background); -} - -.export-actions button { - padding: 8px 16px; - border: 1px solid var(--border-color); - border-radius: 4px; - background: var(--card-background); - color: var(--text-color); - cursor: pointer; - transition: all 0.2s ease; -} - -.export-actions button:hover { - background: var(--header-background); -} - -.export-actions button:last-child { - background: var(--primary-color, #007bff); - color: white; - border-color: var(--primary-color, #007bff); -} - -.export-actions button:last-child:hover { - background: var(--primary-color-dark, #0056b3); -} - -.copy-btn, .close-btn { - padding: 8px 16px; - border: 1px solid var(--border-color); - border-radius: 6px; - background: var(--card-background); - color: var(--text-color); - cursor: pointer; - transition: all 0.2s ease; - font-size: 14px; - display: flex; - align-items: center; - gap: 4px; -} - -.copy-btn:hover { - background: var(--primary-color, #007bff); - color: white; - border-color: var(--primary-color, #007bff); -} - -.close-btn { - background: #dc3545; - color: white; - border-color: #dc3545; - margin-left: auto; -} - -.close-btn:hover { - background: #c82333; - border-color: #bd2130; -} - -.export-details, .export-tips { - margin: 20px 0; - padding: 16px; - border-radius: 6px; - border: 1px solid var(--border-color); -} - -.export-details { - background: var(--header-background); -} - -.export-tips { - background: var(--card-background); - border-color: var(--primary-color, #007bff); - border-left: 4px solid var(--primary-color, #007bff); -} - -.export-details h4, .export-tips h4 { - margin: 0 0 12px 0; - color: var(--text-color); - font-size: 16px; -} - -.export-details ul, .export-tips ul { - margin: 0; - padding-left: 20px; - color: var(--text-color); -} - -.export-details li, .export-tips li { - margin: 8px 0; - line-height: 1.5; -} - -.export-details code, .export-tips code { - background: var(--background-color); - padding: 2px 6px; - border-radius: 3px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 12px; - border: 1px solid var(--border-color); -} - -.export-tips kbd { - background: var(--header-background); - border: 1px solid var(--border-color); - border-radius: 3px; - padding: 2px 6px; - font-family: inherit; - font-size: 12px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); -} - -.copy-feedback { - position: absolute; - top: 10px; - right: 10px; - padding: 8px 12px; - border-radius: 4px; - font-size: 12px; - font-weight: 500; - z-index: 1003; - animation: fadeInOut 2s ease-in-out; -} - -.copy-feedback-success { - background: #d4edda; - color: #155724; - border: 1px solid #c3e6cb; -} - -.copy-feedback-error { - background: #f8d7da; - color: #721c24; - border: 1px solid #f5c6cb; -} - -@keyframes fadeInOut { - 0% { opacity: 0; transform: translateY(-10px); } - 20% { opacity: 1; transform: translateY(0); } - 80% { opacity: 1; transform: translateY(0); } - 100% { opacity: 0; transform: translateY(-10px); } -} - -/* Dark theme adjustments */ -[data-theme="dark"] .selection-checkbox { - background: var(--card-background); - border-color: var(--border-color); -} - -[data-theme="dark"] .grid-item.selected .selection-checkbox { - background: var(--primary-color, #0d6efd); - border-color: var(--primary-color, #0d6efd); -} - -[data-theme="dark"] .export-message-info { - background: #0c5460; - color: #d1ecf1; - border-color: #086972; -} - -[data-theme="dark"] .export-message-success { - background: #155724; - color: #d4edda; - border-color: #1e7e34; -} - -[data-theme="dark"] .export-message-warning { - background: #856404; - color: #fff3cd; - border-color: #b58b14; -} - -[data-theme="dark"] .export-message-error { - background: #721c24; - color: #f8d7da; - border-color: #a94442; -} - -[data-theme="dark"] .copy-feedback-success { - background: #155724; - color: #d4edda; - border-color: #1e7e34; -} - -[data-theme="dark"] .copy-feedback-error { - background: #721c24; - color: #f8d7da; - border-color: #a94442; -} - -[data-theme="dark"] .export-tips kbd { - background: var(--background-color); - color: var(--text-color); -} diff --git a/gallery/assets/css/main.css b/gallery/assets/css/main.css index c6019d7..b9dfb44 100644 --- a/gallery/assets/css/main.css +++ b/gallery/assets/css/main.css @@ -18,7 +18,6 @@ @import url('./metadata.css'); @import url('./metadata-section.css'); @import url('./folder-metadata.css'); -@import url('./export.css'); /* View controls - must come after grid.css to override */ @import url('./view-controls.css'); diff --git a/gallery/assets/css/sort-debug.css b/gallery/assets/css/sort-debug.css deleted file mode 100644 index e69de29..0000000 diff --git a/gallery/assets/js/export-manager.js b/gallery/assets/js/export-manager.js deleted file mode 100644 index ab221c3..0000000 --- a/gallery/assets/js/export-manager.js +++ /dev/null @@ -1,444 +0,0 @@ -/** - * Export Manager for Gallery - * Handles exporting selected plots to merged PDF - */ - -export class ExportManager { - constructor() { - this.selectedPlots = new Set(); - this.maxPlots = 4; - this.init(); - } - - init() { - this.createExportButton(); - this.bindEvents(); - } - - /** - * Create the export button in the floating buttons section - */ - createExportButton() { - const floatingButtons = document.querySelector('.floating-buttons'); - if (!floatingButtons) return; - - const exportBtn = document.createElement('button'); - exportBtn.className = 'floating-btn export-btn'; - exportBtn.id = 'exportBtn'; - exportBtn.title = 'Export Selected Plots (Ctrl+E)'; - exportBtn.innerHTML = '📄'; - exportBtn.style.display = 'none'; // Hidden by default - exportBtn.onclick = () => this.exportSelectedPlots(); - - floatingButtons.appendChild(exportBtn); - - // Add selection counter - const selectionCounter = document.createElement('div'); - selectionCounter.className = 'selection-counter'; - selectionCounter.id = 'selectionCounter'; - selectionCounter.style.display = 'none'; - selectionCounter.innerHTML = '0/4 selected'; - floatingButtons.appendChild(selectionCounter); - } - - /** - * Bind events for plot selection - */ - bindEvents() { - // Add selection mode toggle - document.addEventListener('keydown', (e) => { - if (e.ctrlKey && e.key === 'e') { - e.preventDefault(); - this.toggleSelectionMode(); - } - if (e.key === 'Escape') { - this.exitSelectionMode(); - } - }); - - // Add selection handlers to existing plots - this.addSelectionHandlers(); - } - - /** - * Add selection handlers to all plot items - */ - addSelectionHandlers() { - const plotItems = document.querySelectorAll('.grid-item'); - plotItems.forEach(item => this.addSelectionHandler(item)); - } - - /** - * Add selection handler to a single plot item - */ - addSelectionHandler(item) { - // Create selection overlay - const overlay = document.createElement('div'); - overlay.className = 'selection-overlay'; - overlay.innerHTML = ` -
- -
- `; - item.appendChild(overlay); - - // Add click handler for selection - overlay.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - this.togglePlotSelection(item); - }); - } - - /** - * Toggle selection mode - */ - toggleSelectionMode() { - const body = document.body; - const isSelectionMode = body.classList.contains('selection-mode'); - - if (isSelectionMode) { - this.exitSelectionMode(); - } else { - this.enterSelectionMode(); - } - } - - /** - * Enter selection mode - */ - enterSelectionMode() { - document.body.classList.add('selection-mode'); - document.getElementById('exportBtn').style.display = 'block'; - document.getElementById('selectionCounter').style.display = 'block'; - this.updateSelectionCounter(); - } - - /** - * Exit selection mode - */ - exitSelectionMode() { - const wasInSelectionMode = document.body.classList.contains('selection-mode'); - - document.body.classList.remove('selection-mode'); - document.getElementById('exportBtn').style.display = 'none'; - document.getElementById('selectionCounter').style.display = 'none'; - this.clearSelection(); - - // Show message if user was actually in selection mode - if (wasInSelectionMode) { - this.showMessage('Exited selection mode', 'info'); - } - } - - /** - * Toggle plot selection - */ - togglePlotSelection(item) { - const plotName = this.getPlotName(item); - const plotPath = this.getPlotPath(item); - - if (this.selectedPlots.has(plotName)) { - this.selectedPlots.delete(plotName); - item.classList.remove('selected'); - item.querySelector('.checkbox-icon').textContent = '☐'; - } else { - if (this.selectedPlots.size >= this.maxPlots) { - this.showMessage(`Maximum ${this.maxPlots} plots can be selected`, 'warning'); - return; - } - this.selectedPlots.add(plotName); - item.classList.add('selected'); - item.querySelector('.checkbox-icon').textContent = '☑'; - } - - this.updateSelectionCounter(); - } - - /** - * Get plot name from grid item - */ - getPlotName(item) { - const plotName = item.querySelector('.plot-name'); - return plotName ? plotName.textContent.trim() : ''; - } - - /** - * Get plot PDF path from grid item - */ - getPlotPath(item) { - const link = item.querySelector('a[href$=".pdf"]'); - return link ? link.href : ''; - } - - /** - * Update selection counter - */ - updateSelectionCounter() { - const counter = document.getElementById('selectionCounter'); - if (counter) { - counter.textContent = `${this.selectedPlots.size}/${this.maxPlots} selected`; - } - - const exportBtn = document.getElementById('exportBtn'); - if (exportBtn) { - exportBtn.disabled = this.selectedPlots.size === 0; - exportBtn.style.opacity = this.selectedPlots.size === 0 ? '0.5' : '1'; - } - } - - /** - * Clear all selections - */ - clearSelection() { - this.selectedPlots.clear(); - document.querySelectorAll('.grid-item.selected').forEach(item => { - item.classList.remove('selected'); - const checkbox = item.querySelector('.checkbox-icon'); - if (checkbox) checkbox.textContent = '☐'; - }); - this.updateSelectionCounter(); - } - - /** - * Export selected plots to merged PDF - */ - async exportSelectedPlots() { - if (this.selectedPlots.size === 0) { - this.showMessage('No plots selected', 'warning'); - return; - } - - const plotPaths = Array.from(this.selectedPlots).map(plotName => { - const item = Array.from(document.querySelectorAll('.grid-item')) - .find(item => this.getPlotName(item) === plotName); - return this.getPlotPath(item); - }); - - this.showMessage('Preparing export...', 'info'); - - try { - await this.createMergedPDF(plotPaths); - } catch (error) { - this.showMessage('Export failed: ' + error.message, 'error'); - } - } - - /** - * Create merged PDF using Python script - */ - async createMergedPDF(plotPaths) { - // Convert file:// URLs to actual paths - const actualPaths = plotPaths.map(url => { - if (url.startsWith('file://')) { - return url.substring(7); // Remove 'file://' prefix - } - return url; - }); - - const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0]; - const outputName = `merged_plots_${timestamp}.pdf`; - - const payload = { - plots: actualPaths, - layout: this.calculateLayout(actualPaths.length), - output_name: outputName - }; - - // Generate a unique temporary filename - const tempFileName = `export_request_${Date.now()}.json`; - - // Save the request to a JSON file that can be picked up by a Python script - const requestData = JSON.stringify(payload, null, 2); - - // Show improved export instructions with full command - this.showExportInstructions(requestData, tempFileName); - } - - /** - * Calculate optimal layout for given number of plots - */ - calculateLayout(numPlots) { - switch (numPlots) { - case 1: return { rows: 1, cols: 1 }; - case 2: return { rows: 1, cols: 2 }; - case 3: return { rows: 2, cols: 2 }; // 3 plots in 2x2 grid with one empty - case 4: return { rows: 2, cols: 2 }; - default: return { rows: 2, cols: 2 }; - } - } - - /** - * Show export instructions to user - */ - showExportInstructions(requestData, tempFileName) { - const tempFilePath = `/tmp/${tempFileName}`; - const fullCommand = `echo '${requestData.replace(/'/g, "'\\''")}' > ${tempFilePath} && python export_plots.py ${tempFilePath}`; - - const instructions = ` -
-

🚀 Export Selected Plots

-

Run the following command in your terminal to export the selected plots:

- -
-
- ${fullCommand} -
-
- - - -
-
- -
-

📋 Command Breakdown:

-
    -
  • Creates temporary file: ${tempFilePath}
  • -
  • Runs export script: python export_plots.py
  • -
  • Output file: Will be saved in the work directory
  • -
-
- -
-

💡 Tips:

-
    -
  • The temporary JSON file will be automatically cleaned up after successful export
  • -
  • Use Esc to exit selection mode
  • -
  • Press Ctrl+E to toggle selection mode
  • -
-
-
- `; - - const overlay = document.createElement('div'); - overlay.className = 'export-overlay'; - overlay.innerHTML = instructions; - - // Add methods to the overlay for button handlers - overlay.copyCommand = function() { - navigator.clipboard.writeText(fullCommand).then(() => { - this.showCopyFeedback('Command copied to clipboard!'); - }).catch(() => { - this.showCopyFeedback('Failed to copy. Please select and copy manually.', 'error'); - }); - }; - - overlay.copyJSON = function() { - navigator.clipboard.writeText(requestData).then(() => { - this.showCopyFeedback('JSON copied to clipboard!'); - }).catch(() => { - this.showCopyFeedback('Failed to copy. Please select and copy manually.', 'error'); - }); - }; - - overlay.close = function() { - this.remove(); - }; - - overlay.showCopyFeedback = function(message, type = 'success') { - const feedback = document.createElement('div'); - feedback.className = `copy-feedback copy-feedback-${type}`; - feedback.textContent = message; - this.appendChild(feedback); - - setTimeout(() => { - if (feedback.parentNode) { - feedback.parentNode.removeChild(feedback); - } - }, 2000); - }; - - document.body.appendChild(overlay); - - // Close on ESC key - const handleEscape = (e) => { - if (e.key === 'Escape') { - overlay.remove(); - document.removeEventListener('keydown', handleEscape); - } - }; - document.addEventListener('keydown', handleEscape); - - // Close on clicking outside - overlay.addEventListener('click', (e) => { - if (e.target === overlay) { - overlay.remove(); - document.removeEventListener('keydown', handleEscape); - } - }); - } - - /** - * Download the PDF blob - */ - downloadPDF(blob) { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `merged_plots_${new Date().toISOString().split('T')[0]}.pdf`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - } - - /** - * Show temporary message to user - */ - showMessage(text, type = 'info') { - // Remove existing message - const existing = document.querySelector('.export-message'); - if (existing) existing.remove(); - - const message = document.createElement('div'); - message.className = `export-message export-message-${type}`; - message.textContent = text; - - document.body.appendChild(message); - - setTimeout(() => { - if (message.parentNode) { - message.parentNode.removeChild(message); - } - }, 3000); - } -} - -// Add these methods to ExportManager if not present -ExportManager.prototype.isSelectionModeActive = function() { - return document.body.classList.contains('selection-mode'); -}; -ExportManager.prototype.exitSelectionMode = function() { - document.body.classList.remove('selection-mode'); - if (typeof this.clearSelection === 'function') { - this.clearSelection(); - } -}; - -// Ensure a single global instance -window.exportManager = window.exportManager || new ExportManager(); - -// Listen for ESC key globally to exit selection mode -// (This will work even if focus is not on a plot) -document.addEventListener('keydown', function(e) { - if (e.key === 'Escape' && window.exportManager && window.exportManager.isSelectionModeActive()) { - window.exportManager.exitSelectionMode(); - } -}); - -// Attach improved export logic to export button -document.addEventListener('DOMContentLoaded', function() { - const exportBtn = document.getElementById('exportBtn'); - if (exportBtn) { - exportBtn.addEventListener('click', function() { - window.exportManager.exportSelectedPlots(); - }); - } -}); diff --git a/gallery/config/__init__.py b/gallery/config/__init__.py index 44f5426..54840b8 100644 --- a/gallery/config/__init__.py +++ b/gallery/config/__init__.py @@ -167,7 +167,6 @@ class GalleryConfig: plot_root: str = GalleryDefaults.plot_root cache_enabled: bool = GalleryDefaults.cache_enabled inherit_from_parent: bool = GalleryDefaults.inherit_from_parent - backup_folder: str = "" def __post_init__(self): if isinstance(self.web_folder, str): @@ -215,7 +214,6 @@ class GalleryConfig: plot_root=gallery_cfg.get("plot_root", GalleryDefaults.plot_root), cache_enabled=metadata_cfg.get("cache_enabled", GalleryDefaults.cache_enabled), inherit_from_parent=metadata_cfg.get("inherit_from_parent", GalleryDefaults.inherit_from_parent), - backup_folder=gallery_cfg.get("backup_folder", ""), ) def to_yaml(self, yaml_file: Union[str, Path]) -> None: @@ -229,7 +227,6 @@ class GalleryConfig: "gallery": { "plot_root": self.plot_root, "png_dpi": self.png_dpi, - "backup_folder": self.backup_folder, }, "ui": { "max_recent_plots": 20, diff --git a/gallery/config/config.yaml b/gallery/config/config.yaml index 833c266..04f80a0 100644 --- a/gallery/config/config.yaml +++ b/gallery/config/config.yaml @@ -1,5 +1,4 @@ gallery: - backup_folder: '' plot_root: gallery png_dpi: 400 metadata: diff --git a/gallery/tui.py b/gallery/tui.py index d236611..7169208 100644 --- a/gallery/tui.py +++ b/gallery/tui.py @@ -41,7 +41,6 @@ CONFIG_FIELDS = [ ("web-folder", "paths.web_folder", True), ("plot-root", "gallery.plot_root", False), ("png-dpi", "gallery.png_dpi", False), - ("backup-folder", "gallery.backup_folder", False), ("cache-enabled", "metadata.cache_enabled", False), ("inherit-meta", "metadata.inherit_from_parent", False), ] @@ -250,9 +249,6 @@ class GalleryTUI(App): with Horizontal(classes="field-row"): yield Label("PNG DPI") yield Input(id="png-dpi", placeholder="400") - with Horizontal(classes="field-row"): - yield Label("Backup folder") - yield Input(id="backup-folder", placeholder="leave empty to disable") with Horizontal(classes="field-row"): yield Label("Cache metadata") yield Input(id="cache-enabled", placeholder="true") diff --git a/gallery/utils/backup.py b/gallery/utils/backup.py deleted file mode 100644 index 14ab225..0000000 --- a/gallery/utils/backup.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Backup utilities for gallery.""" - -import datetime -import zipfile -from pathlib import Path - - -def create_backup(web_folder: Path, backup_folder: Path) -> bool: - """ - Create a backup of the web folder. - - Args: - web_folder: Path to the web folder to backup - backup_folder: Path to the backup directory - - Returns: - True if backup was created successfully, False otherwise - """ - try: - today = datetime.date.today().strftime("%Y%m%d") - backup_name = f"backup-{today}.zip" - backup_path = backup_folder / backup_name - - backup_folder.mkdir(parents=True, exist_ok=True) - - if backup_path.exists(): - return True - - with zipfile.ZipFile(backup_path, "w", zipfile.ZIP_DEFLATED) as zipf: - for path in web_folder.rglob("*"): - if path.is_file(): - arcname = path.relative_to(web_folder.parent) - zipf.write(path, arcname) - return True - except Exception as e: - print(f"Warning: Could not create backup: {e}") - return False diff --git a/generate_gallery.py b/generate_gallery.py deleted file mode 100644 index 15873b7..0000000 --- a/generate_gallery.py +++ /dev/null @@ -1,489 +0,0 @@ -""" -Scientific Gallery Generator - -This module generates static HTML galleries from PDF plots. -It converts PDF plots to PNG thumbnails, creates responsive web interfaces, -and organizes plots into hierarchical directory structures. - -Features: -- PDF to PNG conversion with configurable DPI -- Incremental updates (only converts when source is newer) -- Jinja2 templating for consistent HTML generation -- Support for nested folder structures -- Responsive grid layout with search and navigation -""" - -import subprocess -import shutil -import os -import sys -import argparse -import time -from pathlib import Path -from typing import Dict, Any, Optional -from datetime import datetime -from jinja2 import Environment, FileSystemLoader - -from orchestration.config import Config -from orchestration.logger import GalleryLogger, create_logger -from orchestration.metadata import ( - load_folder_metadata, - merge_metadata, - resolve_metadata_for_plot, - save_metadata_cache, - get_metadata_file_path -) - - -CONFIG = Config.from_yaml("config.yaml") - - -def datetime_from_timestamp(timestamp): - """Convert a Unix timestamp to a datetime object.""" - return datetime.fromtimestamp(timestamp) - - -def strftime_filter(dt, fmt): - """Format a datetime object using strftime.""" - return dt.strftime(fmt) - - -env = Environment(loader=FileSystemLoader(".")) -env.filters['datetime_from_timestamp'] = datetime_from_timestamp -env.filters['strftime'] = strftime_filter -template = env.get_template("templates/gallery.html") - - -def convert_pdf_to_png(pdf_path: Path, logger: GalleryLogger) -> None: - """ - Convert a PDF file to PNG format using ImageMagick. - - Only converts if the PNG doesn't exist or if the PDF is newer than - the PNG (with a 30-second buffer to handle filesystem timing issues). - - Args: - pdf_path: Path to the source PDF file - - Raises: - subprocess.CalledProcessError: If ImageMagick conversion fails - """ - png_path = pdf_path.with_suffix(".png") - - if png_path.exists(): - pdf_mtime = pdf_path.stat().st_mtime - png_mtime = png_path.stat().st_mtime - if png_mtime >= (pdf_mtime + 30): - logger.skipped_pdf(pdf_path.name) - return - else: - logger.debug(f"PDF {pdf_path.name} is newer than PNG, reconverting...") - - # Perform conversion - start_time = time.time() - logger.debug(f"Converting {pdf_path} → {png_path}") - - try: - subprocess.run([ - "convert", - "-density", str(CONFIG.png_dpi), - str(pdf_path), - "-quality", "95", - str(png_path) - ], check=True) - - duration = time.time() - start_time - logger.converted_pdf(pdf_path.name, duration) - - except subprocess.CalledProcessError as e: - logger.error(f"Failed to convert {pdf_path.name}: {e}") - raise - - -def needs_update(source_file: Path, target_file: Path) -> bool: - """ - Check if target file needs updating based on source modification time. - - Args: - source_file: Path to the source file - target_file: Path to the target file - - Returns: - True if target needs update, False otherwise - """ - if not target_file.exists(): - return True - - source_mtime = source_file.stat().st_mtime - target_mtime = target_file.stat().st_mtime - - return source_mtime > (target_mtime + 30) - - -def build_gallery(source_dir: Path, web_dir: Path, - relative_path: Path = None, - inherited_metadata: Optional[Dict[str, Any]] = None, - logger: Optional[GalleryLogger] = None) -> None: - """ - Recursively build gallery structure from source directory. - - Processes all PDF files in the source directory, converts them to PNG, - copies both to the web directory, and generates index.html files with - navigation and thumbnails. Now includes metadata support. - - Args: - source_dir: Source directory containing PDF files - web_dir: Target web directory for gallery output - relative_path: Relative path from gallery root (for navigation) - inherited_metadata: Metadata inherited from parent directories - """ - if relative_path is None: - relative_path = Path(".") - - if inherited_metadata is None: - inherited_metadata = {} - - if logger is None: - logger = GalleryLogger(verbose=False) - - # Load folder-level metadata and merge with inherited metadata - folder_metadata = load_folder_metadata(source_dir) - current_metadata = merge_metadata(inherited_metadata, folder_metadata) - - # Log metadata discovery - if folder_metadata: - metadata_file_path = get_metadata_file_path(source_dir) - metadata_path = Path(metadata_file_path) - if metadata_path.exists(): - logger.found_metadata(metadata_path.name, len(folder_metadata)) - - pdf_files = list(source_dir.glob("*.pdf")) - subdirs = [d for d in source_dir.iterdir() if d.is_dir()] - - # Log directory discovery - if pdf_files: - logger.found_directory(source_dir.name if source_dir.name else "root", len(pdf_files)) - - # Log individual PDF discovery in verbose mode - for pdf_file in pdf_files: - logger.found_pdf(pdf_file.name) - - items = [] - plot_metadata_cache = {} - - for pdf_file in pdf_files: - png_file = pdf_file.with_suffix(".png") - - web_pdf = web_dir / pdf_file.name - web_png = web_dir / png_file.name - - # Copy PDF if needed - if needs_update(pdf_file, web_pdf): - logger.debug(f"Copying {pdf_file} to {web_pdf}") - shutil.copy2(pdf_file, web_pdf) - else: - logger.debug(f"Skipping {pdf_file.name} (PDF up to date)") - - # Convert PDF to PNG if needed - if not png_file.exists(): - convert_pdf_to_png(pdf_file, logger) - - if needs_update(png_file, web_png): - logger.debug(f"Copying {png_file} to {web_png}") - shutil.copy2(png_file, web_png) - else: - logger.debug(f"Skipping {png_file.name} (PNG up to date)") - - # Resolve metadata for this specific plot - plot_metadata = resolve_metadata_for_plot(pdf_file, current_metadata) - plot_metadata_cache[pdf_file.stem] = plot_metadata - - # Get source file creation time (in seconds since epoch) - source_creation_time = int(pdf_file.stat().st_ctime) - - items.append({ - "name": pdf_file.stem, - "pdf_href": pdf_file.name, - "png_href": png_file.name, - "metadata": plot_metadata, - "creation_time": source_creation_time - }) - - # Save metadata cache for this directory - save_metadata_cache(web_dir, plot_metadata_cache) - - subdir_names = [] - for subdir in subdirs: - subdir_web = web_dir / subdir.name - subdir_web.mkdir(exist_ok=True) - subdir_relative = relative_path / subdir.name - # Pass logger and current metadata to subdirectories - build_gallery(subdir, subdir_web, subdir_relative, current_metadata, logger) - subdir_names.append(subdir.name) - - output_html = web_dir / "index.html" - - # Always regenerate HTML to ensure subdirectory changes are reflected - # This ensures new subdirectories appear in navigation - force_regeneration = True - if output_html.exists() and not force_regeneration: - html_mtime = output_html.stat().st_mtime - # Check if any subdirectory is newer than the HTML file - for subdir in subdirs: - if subdir.stat().st_mtime > html_mtime: - logger.debug(f"Subdirectory {subdir.name} is newer, forcing regeneration") - break - - if relative_path == Path("."): - title = "Gallery" - else: - title = f"Gallery: {relative_path}" - - # Calculate statistics for current directory - current_stats = calculate_directory_stats(web_dir) - stats = { - "file_count": len(items), - "folder_count": len(subdir_names), - "total_size": format_file_size(current_stats["total_size"]), - "total_size_bytes": current_stats["total_size"] - } - - # Calculate relative path to assets based on directory depth - if relative_path == Path("."): - assets_path = "../assets" - else: - # Count directory levels to go back to gallery, then to public_html - depth = len(relative_path.parts) - assets_path = "../" * (depth + 1) + "assets" - - with output_html.open("w") as f: - rendered_html = template.render( - title=title, - items=items, - subdirs=subdir_names, - relpath=str(relative_path), - paths=CONFIG.paths, - ui=CONFIG.ui, - stats=stats, - folder_metadata=current_metadata, - assets_path=assets_path, - source_dir=str(source_dir), - metadata_file_path=get_metadata_file_path(source_dir) - ) - f.write(rendered_html) - - logger.generated_html(str(output_html), len(items)) - - -def calculate_directory_stats(directory: Path) -> dict: - """ - Calculate statistics for a directory. - - Args: - directory: Path to the directory to analyze - - Returns: - Dictionary containing file count, folder count, and total size - """ - stats = { - "file_count": 0, - "folder_count": 0, - "total_size": 0, - "pdf_size": 0, - "png_size": 0 - } - - if not directory.exists(): - return stats - - for item in directory.rglob("*"): - if item.is_file(): - stats["file_count"] += 1 - size = item.stat().st_size - stats["total_size"] += size - - if item.suffix.lower() == '.pdf': - stats["pdf_size"] += size - elif item.suffix.lower() == '.png': - stats["png_size"] += size - elif item.is_dir(): - stats["folder_count"] += 1 - - return stats - - -def format_file_size(size_bytes: int) -> str: - """ - Format file size in human readable format. - - Args: - size_bytes: Size in bytes - - Returns: - Formatted size string - """ - if size_bytes == 0: - return "0 B" - - size_names = ["B", "KB", "MB", "GB", "TB"] - size = float(size_bytes) - i = 0 - while size >= 1024 and i < len(size_names) - 1: - size /= 1024 - i += 1 - - return f"{size:.1f} {size_names[i]}" - - -def refresh_gallery_cgi(): - """ - CGI handler to refresh the gallery from a web request. - Outputs a minimal HTTP response and triggers gallery regeneration. - """ - import traceback - print("Content-Type: text/plain\n") - try: - main() - print("Gallery refreshed successfully.") - except Exception as e: - print(f"Error refreshing gallery: {e}") - traceback.print_exc(file=sys.stdout) - - -def main(clean_first: bool = False, verbose: bool = False) -> None: - """ - Main entry point for gallery generation. - - Args: - clean_first: If True, removes and recreates the gallery directory - verbose: If True, enables verbose logging - - Processes all configured sources and generates the complete gallery - structure in the web directory. Ensures assets are available. - """ - # Initialize the logger with appropriate verbosity - logger = GalleryLogger(verbose=verbose) - - gallery_root = Path(CONFIG.web_folder) / CONFIG.plot_root - - if clean_first and gallery_root.exists(): - logger.info(f"Cleaning gallery directory {gallery_root}...") - shutil.rmtree(gallery_root) - - # Ensure gallery root exists - gallery_root.mkdir(parents=True, exist_ok=True) - - # Always ensure assets are up to date - assets_src = Path("assets") - assets_dst = gallery_root.parent / "assets" - - if assets_src.exists(): - # Update assets if they don't exist or are outdated - main_css_src = assets_src / "css" / "main.css" - main_css_dst = assets_dst / "css" / "main.css" - if not assets_dst.exists() or needs_update(main_css_src, main_css_dst): - if assets_dst.exists(): - shutil.rmtree(assets_dst) - shutil.copytree(assets_src, assets_dst) - logger.assets_updated() - else: - logger.warning(f"Assets directory {assets_src} not found") - - for source in CONFIG.sources: - source_path = Path(source.path) - - if source_path.is_file() and source_path.suffix == '.pdf': - source_web_dir = gallery_root / source.name - source_web_dir.mkdir(parents=True, exist_ok=True) - - pdf_name = source_path.name - png_name = source_path.with_suffix('.png').name - - web_pdf_path = source_web_dir / pdf_name - web_png_path = source_web_dir / png_name - source_png_path = source_path.with_suffix('.png') - - if needs_update(source_path, web_pdf_path): - logger.debug(f"Copying {source_path} to {web_pdf_path}") - shutil.copy2(source_path, web_pdf_path) - else: - logger.debug(f"Skipping {source_path.name} (up to date)") - - if not source_png_path.exists(): - convert_pdf_to_png(source_path, logger) - - if needs_update(source_png_path, web_png_path): - logger.debug(f"Copying {source_png_path} to {web_png_path}") - shutil.copy2(source_png_path, web_png_path) - else: - logger.debug(f"Skipping {source_png_path.name} (up to date)") - - # Get source file creation time for single file - source_creation_time = int(source_path.stat().st_ctime) - - items = [{ - "name": source_path.stem, - "pdf_href": pdf_name, - "png_href": png_name, - "creation_time": source_creation_time - }] - - # Calculate statistics for single file - current_stats = calculate_directory_stats(source_web_dir) - stats = { - "file_count": 1, - "folder_count": 0, - "total_size": format_file_size(current_stats["total_size"]), - "total_size_bytes": current_stats["total_size"] - } - - # Calculate relative path to assets for single file - # Single files are at depth 1 (gallery_root/source.name/index.html) - assets_path = "../assets" - - output_html = source_web_dir / "index.html" - with output_html.open("w") as f: - f.write(template.render( - title=source.name, - items=items, - subdirs=[], - relpath=source.name, - paths=CONFIG.paths, - ui=CONFIG.ui, - stats=stats, - folder_metadata={}, - assets_path=assets_path, - source_dir=str(source_path.parent), - metadata_file_path=get_metadata_file_path(source_path.parent) - )) - - logger.generated_html(str(output_html), 1) - logger.info(f"✓ Completed {source.name}") - - elif source_path.is_dir(): - source_web_dir = gallery_root / source.name - source_web_dir.mkdir(parents=True, exist_ok=True) - build_gallery(source_path, source_web_dir, Path(source.name), {}, logger) - logger.info(f"✓ Completed {source.name}") - else: - logger.warning(f"Source {source.path} is neither a directory nor a PDF file") - - logger.summary(len(CONFIG.sources)) - - -if __name__ == "__main__": - if 'GATEWAY_INTERFACE' in os.environ: - refresh_gallery_cgi() - else: - parser = argparse.ArgumentParser(description='Generate gallery') - parser.add_argument( - '--clean', - action='store_true', - help='Clean gallery directory before generation' - ) - parser.add_argument( - '--verbose', '-v', - action='store_true', - help='Enable verbose logging' - ) - args = parser.parse_args() - main(clean_first=args.clean, verbose=args.verbose) diff --git a/orchestration/backup.py b/orchestration/backup.py deleted file mode 100644 index d1937a4..0000000 --- a/orchestration/backup.py +++ /dev/null @@ -1,23 +0,0 @@ -import zipfile -import datetime -from pathlib import Path - -WEB_FOLDER = Path("plots") -BACKUP_FOLDER = Path("backups") - -today = datetime.date.today().strftime("%Y%m%d") -backup_name = f"backup-{today}.zip" -backup_path = BACKUP_FOLDER / backup_name - -BACKUP_FOLDER.mkdir(parents=True, exist_ok=True) - -if backup_path.exists(): - print(f"Backup already exists: {backup_path}") -else: - print(f"Creating backup: {backup_path}") - with zipfile.ZipFile(backup_path, "w", zipfile.ZIP_DEFLATED) as zipf: - for path in WEB_FOLDER.rglob("*"): - if path.is_file(): - arcname = path.relative_to(WEB_FOLDER.parent) - zipf.write(path, arcname) - print("✅ Backup complete.") diff --git a/orchestration/config.py b/orchestration/config.py deleted file mode 100644 index 9672315..0000000 --- a/orchestration/config.py +++ /dev/null @@ -1,147 +0,0 @@ -""" -Scientific Gallery Configuration Management - -This module provides dataclasses and utilities for managing configuration -of the scientific gallery system, including paths, gallery settings, -UI preferences, and data sources. -""" - -from dataclasses import dataclass, field, asdict -from pathlib import Path -import yaml - - -@dataclass -class PathConfig: - """Configuration for system paths and directories.""" - work_dir: str - web_folder: str - cgi_script: str - config_path: str - - -@dataclass -class GalleryConfig: - """Configuration for gallery generation and display settings.""" - plot_root: str - png_dpi: int - backup_folder: str - - -@dataclass -class UIConfig: - """Configuration for user interface behavior and preferences.""" - max_recent_plots: int - search_debounce_ms: int - - -@dataclass -class MetadataConfig: - """Configuration for metadata handling.""" - cache_enabled: bool = True - inherit_from_parent: bool = True - supported_formats: list[str] = field( - default_factory=lambda: ['.yaml', '.yml', '.json'] - ) - - -@dataclass -class GalleryItem: - """Represents a single data source for the gallery.""" - name: str - path: Path - - -@dataclass -class Config: - """ - Main configuration class that aggregates all gallery settings. - - Provides backward compatibility properties and methods for loading - configuration from YAML files. - """ - paths: PathConfig - gallery: GalleryConfig - ui: UIConfig - metadata: MetadataConfig - sources: list[GalleryItem] = field(default_factory=list) - - @property - def web_folder(self): - """Backward compatibility property for web folder path.""" - return self.paths.web_folder - - @property - def png_dpi(self): - """Backward compatibility property for PNG conversion DPI.""" - return self.gallery.png_dpi - - @property - def plot_root(self): - """Backward compatibility property for plot root directory.""" - return self.gallery.plot_root - - @property - def backup_folder(self): - """Backward compatibility property for backup folder path.""" - return self.gallery.backup_folder - - @classmethod - def from_yaml(cls, yaml_file: str) -> "Config": - """ - Load configuration from a YAML file. - - Args: - yaml_file: Path to the YAML configuration file - - Returns: - Config instance with loaded settings - - Raises: - FileNotFoundError: If the YAML file doesn't exist - yaml.YAMLError: If the YAML file is malformed - """ - with open(yaml_file, "r") as f: - data = yaml.safe_load(f) - - paths_data = data.get('paths', {}) - gallery_data = data.get('gallery', {}) - ui_data = data.get('ui', {}) - metadata_data = data.get('metadata', {}) - sources_data = data.get('sources', []) - - paths = PathConfig(**paths_data) - gallery = GalleryConfig(**gallery_data) - ui = UIConfig(**ui_data) - metadata = MetadataConfig(**metadata_data) - - sources = [ - GalleryItem(name=source["name"], path=Path(source["path"])) - for source in sources_data - ] - - return cls( - paths=paths, - gallery=gallery, - ui=ui, - metadata=metadata, - sources=sources - ) - - def to_yaml(self, yaml_file: str) -> None: - """ - Save the current configuration to a YAML file. - - Args: - yaml_file: Path where to save the YAML configuration - - Raises: - IOError: If unable to write to the specified file - """ - with open(yaml_file, "w") as f: - yaml.dump(asdict(self), f, default_flow_style=False) - - -if __name__ == "__main__": - config = Config.from_yaml("config.yaml") - print("Loaded config successfully:", config) diff --git a/orchestration/logger.py b/orchestration/logger.py deleted file mode 100644 index 8e6de4f..0000000 --- a/orchestration/logger.py +++ /dev/null @@ -1,250 +0,0 @@ -""" -Gallery logging wrapper using Python's built-in logging module with tree-like output. -""" - -import logging -import sys - - -class Colors: - """ANSI color codes for terminal output""" - RESET = '\033[0m' - BOLD = '\033[1m' - - # Standard colors - RED = '\033[31m' - GREEN = '\033[32m' - YELLOW = '\033[33m' - BLUE = '\033[34m' - MAGENTA = '\033[35m' - CYAN = '\033[36m' - WHITE = '\033[37m' - - # Bright colors - BRIGHT_GREEN = '\033[92m' - BRIGHT_YELLOW = '\033[93m' - BRIGHT_BLUE = '\033[94m' - BRIGHT_CYAN = '\033[96m' - - -class TreeFormatter(logging.Formatter): - """Custom formatter that creates clean output with colors""" - - def __init__(self, use_colors: bool = True): - super().__init__() - self.use_colors = use_colors and sys.stdout.isatty() - - def _colorize(self, text: str, color: str) -> str: - """Apply color to text if colors are enabled""" - if not self.use_colors: - return text - return f"{color}{text}{Colors.RESET}" - - def format(self, record): - # Extract custom attributes from the record - indent = getattr(record, 'indent', 0) - - # Create simple indentation - prefix = " " * indent - - # Apply colors based on level and content - message = record.getMessage() - - if record.levelname == 'INFO': - if 'Processing:' in message: - # Main source headers - message = self._colorize(message, Colors.BOLD + Colors.MAGENTA) - elif 'Generated' in message: - message = self._colorize(message, Colors.GREEN) - elif 'PDF files' in message: - message = self._colorize(message, Colors.BLUE) - elif 'Converting' in message: - message = self._colorize(message, Colors.CYAN) - elif 'Completed' in message and 'sources' in message: - message = self._colorize(message, Colors.BOLD + Colors.GREEN) - else: - message = self._colorize(message, Colors.WHITE) - elif record.levelname == 'WARNING': - message = self._colorize(f"WARNING: {message}", Colors.YELLOW) - elif record.levelname == 'ERROR': - message = self._colorize(f"ERROR: {message}", Colors.RED) - elif record.levelname == 'DEBUG': - message = self._colorize(message, Colors.CYAN) - else: - message = message - - return f"{prefix}{message}" - - -class GalleryLogger: - """ - Simple logger for gallery generation using Python's logging module. - Provides convenient methods for common logging patterns with clean output. - """ - - def __init__(self, name: str = "gallery", level: int = logging.INFO, - verbose: bool = False, use_colors: bool = True): - """ - Initialize the gallery logger. - - Args: - name: Logger name - level: Logging level (default: INFO) - verbose: If True, enables DEBUG level logging - use_colors: If True, enables colored output - """ - self.logger = logging.getLogger(name) - self.use_colors = use_colors - self.current_source = None - self.current_depth = 0 - - # Set level based on verbose flag - if verbose: - self.logger.setLevel(logging.DEBUG) - else: - self.logger.setLevel(level) - - # Only add handler if logger doesn't have one already - if not self.logger.handlers: - handler = logging.StreamHandler(sys.stdout) - formatter = TreeFormatter(use_colors=use_colors) - handler.setFormatter(formatter) - self.logger.addHandler(handler) - - # Prevent propagation to avoid duplicate messages - self.logger.propagate = False - - def _log_with_tree(self, level: int, message: str, indent: int = 0): - """Log a message with simple indentation""" - # Store indentation info in a way the formatter can access - original_makeRecord = self.logger.makeRecord - - def makeRecord_with_indent(*args, **kwargs): - record = original_makeRecord(*args, **kwargs) - record.indent = indent - return record - - # Temporarily replace makeRecord to add our custom attributes - self.logger.makeRecord = makeRecord_with_indent - try: - self.logger.log(level, message) - finally: - # Restore original makeRecord - self.logger.makeRecord = original_makeRecord - - def start_source(self, source_name: str): - """Begin processing a new source""" - self.current_source = source_name - self.current_depth = 0 - if self.use_colors: - colored_name = f"{Colors.BOLD}{Colors.MAGENTA}{source_name}{Colors.RESET}" - self.logger.info(f"\nProcessing: {colored_name}") - else: - self.logger.info(f"\nProcessing: {source_name}") - - def info(self, message: str, indent: int = 0): - """Log an info message""" - self._log_with_tree(logging.INFO, message, indent) - - def debug(self, message: str, indent: int = 0): - """Log a debug message""" - self._log_with_tree(logging.DEBUG, message, indent) - - def warning(self, message: str, indent: int = 0): - """Log a warning message""" - self._log_with_tree(logging.WARNING, message, indent) - - def error(self, message: str, indent: int = 0): - """Log an error message""" - self._log_with_tree(logging.ERROR, message, indent) - - def found_directory(self, dir_name: str, pdf_count: int, indent: int = 1): - """Log discovery of a directory with PDFs""" - if pdf_count > 0: - self._log_with_tree( - logging.INFO, - f"Found {dir_name}/ ({pdf_count} PDFs)", - indent - ) - - def found_pdf(self, pdf_name: str, indent: int = 2): - """Log discovery of a PDF file (debug only)""" - self._log_with_tree(logging.DEBUG, f"Found PDF: {pdf_name}", indent) - - def found_metadata(self, metadata_file: str, field_count: int, indent: int = 1): - """Log discovery of a metadata file""" - self._log_with_tree( - logging.INFO, - f"Found metadata: {metadata_file} ({field_count} fields)", - indent - ) - - def converted_pdf(self, pdf_name: str, duration: float = None, indent: int = 2): - """Log successful PDF conversion""" - if duration is not None: - self._log_with_tree( - logging.INFO, - f"Converting {pdf_name} ({duration:.2f}s)", - indent - ) - else: - self._log_with_tree(logging.INFO, f"Converting {pdf_name}", indent) - - def skipped_pdf(self, pdf_name: str, reason: str = "up-to-date", indent: int = 2): - """Log skipped PDF conversion""" - self._log_with_tree(logging.DEBUG, f"Skipping {pdf_name} ({reason})", indent) - - def generated_html(self, html_path: str, plot_count: int, indent: int = 1): - """Log HTML page generation""" - # Extract just the meaningful part of the path - if '/gallery/' in html_path: - short_path = html_path.split('/gallery/')[-1] - else: - short_path = html_path - - if plot_count > 0: - self._log_with_tree( - logging.INFO, - f"Generated {short_path} ({plot_count} plots)", - indent - ) - else: - self._log_with_tree( - logging.DEBUG, - f"Generated {short_path} (index only)", - indent - ) - - def assets_updated(self): - """Log assets update""" - self.logger.info("Assets updated") - - def summary(self, source_count: int): - """Print a summary""" - if self.use_colors: - message = f"{Colors.BOLD}{Colors.GREEN}Completed processing {source_count} sources{Colors.RESET}" - else: - message = f"Completed processing {source_count} sources" - self.logger.info(f"\n{message}") - - -def create_logger(verbose: bool = False, quiet: bool = False, use_colors: bool = True) -> GalleryLogger: - """ - Create a configured logger for the gallery application. - - Args: - verbose: Enable debug-level logging - quiet: Suppress most output (only errors and warnings) - use_colors: Enable colored output - - Returns: - Configured GalleryLogger instance - """ - if quiet: - level = logging.WARNING - elif verbose: - level = logging.DEBUG - else: - level = logging.INFO - - return GalleryLogger(level=level, verbose=verbose, use_colors=use_colors) diff --git a/orchestration/metadata.py b/orchestration/metadata.py deleted file mode 100644 index deb13d3..0000000 --- a/orchestration/metadata.py +++ /dev/null @@ -1,159 +0,0 @@ -""" -Metadata Management for Scientific Gallery Generator - -This module handles loading, parsing, and caching of metadata for plots -and folders in the gallery system. Supports YAML and JSON formats with -hierarchical inheritance. - -Features: -- Load metadata from YAML/JSON files -- Hierarchical metadata inheritance from parent folders -- Plot-specific metadata overrides -- Metadata caching for performance -""" - -import json -import yaml -import logging -from pathlib import Path -from typing import Dict, Any - - -def load_metadata_file(metadata_path: Path) -> Dict[str, Any]: - """ - Load metadata from a YAML or JSON file. - - Args: - metadata_path: Path to the metadata file - - Returns: - Dictionary containing the metadata, empty dict if file doesn't exist - or can't be parsed - """ - if not metadata_path.exists(): - return {} - - try: - with metadata_path.open('r', encoding='utf-8') as f: - suffix_lower = metadata_path.suffix.lower() - if suffix_lower == '.yaml' or suffix_lower == '.yml': - return yaml.safe_load(f) or {} - elif metadata_path.suffix.lower() == '.json': - return json.load(f) or {} - else: - logging.warning(f"Unknown metadata file format: {metadata_path}") - return {} - except (yaml.YAMLError, json.JSONDecodeError, IOError) as e: - logging.warning(f"Could not parse metadata file {metadata_path}: {e}") - raise e - - -def load_folder_metadata(folder_path: Path) -> Dict[str, Any]: - """ - Load folder-level metadata from metadata.yaml, metadata.yml, or metadata.json. - - Args: - folder_path: Path to the folder to check for metadata - - Returns: - Dictionary containing the folder metadata - """ - # Try YAML first, then JSON for backwards compatibility - for filename in ['metadata.yaml', 'metadata.yml', 'metadata.json']: - metadata_path = folder_path / filename - if metadata_path.exists(): - return load_metadata_file(metadata_path) - - return {} - - -def get_metadata_file_path(folder_path: Path) -> str: - """ - Get the metadata file path for a folder. Returns existing file if found, - otherwise suggests metadata.yaml (preferred format). - - Args: - folder_path: Path to the folder to check for metadata - - Returns: - String path to the metadata file (existing or suggested) - """ - # Preferred order: YAML first, then JSON - preferred_files = ['metadata.yaml', 'metadata.yml', 'metadata.json'] - - for filename in preferred_files: - metadata_path = folder_path / filename - if metadata_path.exists(): - return str(metadata_path) - - # If no file exists, suggest metadata.yaml (preferred format) - return str(folder_path / 'metadata.yaml') - - -def merge_metadata( - parent_metadata: Dict[str, Any], - child_metadata: Dict[str, Any] -) -> Dict[str, Any]: - """ - Merge parent and child metadata, with child values overriding parent - values. - - Args: - parent_metadata: Metadata from parent folder - child_metadata: Metadata from current folder - - Returns: - Merged metadata dictionary - """ - merged = parent_metadata.copy() - merged.update(child_metadata) - return merged - - -def resolve_metadata_for_plot( - plot_path: Path, - inherited_metadata: Dict[str, Any] -) -> Dict[str, Any]: - """ - Resolve metadata for a specific plot by merging inherited metadata - with plot-specific metadata. - - Args: - plot_path: Path to the plot file (PDF) - inherited_metadata: Metadata inherited from folder hierarchy - - Returns: - Final merged metadata for the plot - """ - plot_stem = plot_path.stem - plot_dir = plot_path.parent - - # Check for plot-specific metadata files - for suffix in ['.yaml', '.yml', '.json']: - plot_metadata_path = plot_dir / f"{plot_stem}{suffix}" - if plot_metadata_path.exists(): - plot_metadata = load_metadata_file(plot_metadata_path) - return merge_metadata(inherited_metadata, plot_metadata) - - # No plot-specific metadata found, return inherited metadata - return inherited_metadata.copy() - - -def save_metadata_cache( - web_dir: Path, - plot_metadata_cache: Dict[str, Dict[str, Any]] -) -> None: - """ - Save plot metadata cache to meta_cache.json in the web directory. - - Args: - web_dir: Web directory where the cache file should be saved - plot_metadata_cache: Dictionary mapping plot names to their metadata - """ - cache_path = web_dir / "meta_cache.json" - try: - with cache_path.open('w', encoding='utf-8') as f: - json.dump(plot_metadata_cache, f, indent=2, ensure_ascii=False) - logging.debug(f"Saved metadata cache: {cache_path}") - except IOError as e: - logging.warning(f"Could not save metadata cache {cache_path}: {e}") diff --git a/orchestration/pdf_export.py b/orchestration/pdf_export.py deleted file mode 100644 index 51df501..0000000 --- a/orchestration/pdf_export.py +++ /dev/null @@ -1,205 +0,0 @@ -""" -PDF Export Module for Scientific Gallery Generator - -This module handles exporting and merging multiple plots into a single PDF -using PyPDF2 and reportlab for layout. -""" - -import io -import tempfile -from pathlib import Path -from typing import List, Dict, Any -import subprocess - -try: - from PyPDF2 import PdfReader, PdfWriter - from reportlab.pdfgen import canvas - from reportlab.lib.pagesizes import letter, A4 - from reportlab.lib.utils import ImageReader - HAS_PDF_LIBS = True -except ImportError: - HAS_PDF_LIBS = False - - -class PDFExporter: - """Handles exporting multiple plots to a merged PDF""" - - def __init__(self): - self.page_size = A4 - self.margin = 50 - - def merge_plots(self, plot_paths: List[str], layout: Dict[str, int], - output_path: str = None) -> bytes: - """ - Merge multiple PDF plots into a single PDF with grid layout. - - Args: - plot_paths: List of paths to PDF files - layout: Dictionary with 'rows' and 'cols' keys - output_path: Optional output file path - - Returns: - PDF bytes - """ - if not HAS_PDF_LIBS: - return self._merge_with_pdfjam(plot_paths, layout, output_path) - - return self._merge_with_pypdf(plot_paths, layout, output_path) - - def _merge_with_pypdf(self, plot_paths: List[str], layout: Dict[str, int], - output_path: str = None) -> bytes: - """Merge PDFs using PyPDF2 and reportlab""" - from reportlab.pdfgen import canvas - from reportlab.lib.pagesizes import A4 - - # Create a new PDF with the layout - buffer = io.BytesIO() - c = canvas.Canvas(buffer, pagesize=A4) - page_width, page_height = A4 - - rows = layout['rows'] - cols = layout['cols'] - - # Calculate dimensions for each plot - plot_width = (page_width - 2 * self.margin) / cols - plot_height = (page_height - 2 * self.margin) / rows - - # Place each plot in the grid - for i, plot_path in enumerate(plot_paths): - if i >= rows * cols: - break - - row = i // cols - col = i % cols - - # Calculate position - x = self.margin + col * plot_width - y = page_height - self.margin - (row + 1) * plot_height - - try: - # Read the source PDF - with open(plot_path, 'rb') as f: - reader = PdfReader(f) - if len(reader.pages) > 0: - page = reader.pages[0] - - # Convert PDF page to image and place it - # This is a simplified approach - in practice you'd want - # to properly scale and position the PDF content - self._draw_pdf_placeholder(c, x, y, plot_width, plot_height, - Path(plot_path).stem) - - except Exception as e: - print(f"Error processing {plot_path}: {e}") - self._draw_error_placeholder(c, x, y, plot_width, plot_height) - - c.save() - - if output_path: - with open(output_path, 'wb') as f: - f.write(buffer.getvalue()) - - return buffer.getvalue() - - def _merge_with_pdfjam(self, plot_paths: List[str], layout: Dict[str, int], - output_path: str = None) -> bytes: - """Merge PDFs using pdfjam (requires pdfpages LaTeX package)""" - rows = layout['rows'] - cols = layout['cols'] - - # Create temporary output file - with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp_file: - temp_output = tmp_file.name - - try: - # Build pdfjam command - cmd = [ - 'pdfjam', - '--nup', f'{cols}x{rows}', - '--landscape' if cols > rows else '--no-landscape', - '--frame', 'true', - '--delta', '10pt 10pt', - '--offset', '0pt 0pt', - '--outfile', temp_output - ] - - # Add input files - cmd.extend(plot_paths[:rows * cols]) - - # Run pdfjam - result = subprocess.run(cmd, capture_output=True, text=True) - - if result.returncode != 0: - raise Exception(f"pdfjam failed: {result.stderr}") - - # Read the output file - with open(temp_output, 'rb') as f: - pdf_bytes = f.read() - - if output_path: - with open(output_path, 'wb') as f: - f.write(pdf_bytes) - - return pdf_bytes - - finally: - # Clean up temporary file - Path(temp_output).unlink(missing_ok=True) - - def _draw_pdf_placeholder(self, canvas, x: float, y: float, width: float, - height: float, plot_name: str): - """Draw a placeholder for a PDF plot""" - # Draw border - canvas.setStrokeColorRGB(0.5, 0.5, 0.5) - canvas.setLineWidth(1) - canvas.rect(x, y, width, height) - - # Draw plot name - canvas.setFillColorRGB(0, 0, 0) - canvas.setFont("Helvetica", 10) - text_width = canvas.stringWidth(plot_name, "Helvetica", 10) - text_x = x + (width - text_width) / 2 - text_y = y + height / 2 - canvas.drawString(text_x, text_y, plot_name) - - def _draw_error_placeholder(self, canvas, x: float, y: float, width: float, - height: float): - """Draw an error placeholder""" - # Draw red border - canvas.setStrokeColorRGB(1, 0, 0) - canvas.setLineWidth(2) - canvas.rect(x, y, width, height) - - # Draw error text - canvas.setFillColorRGB(1, 0, 0) - canvas.setFont("Helvetica-Bold", 12) - error_text = "Error loading plot" - text_width = canvas.stringWidth(error_text, "Helvetica-Bold", 12) - text_x = x + (width - text_width) / 2 - text_y = y + height / 2 - canvas.drawString(text_x, text_y, error_text) - - -def check_dependencies() -> Dict[str, bool]: - """Check if required dependencies are available""" - deps = { - 'pypdf2': HAS_PDF_LIBS, - 'pdfjam': False - } - - # Check for pdfjam - try: - result = subprocess.run(['pdfjam', '--version'], - capture_output=True, text=True) - deps['pdfjam'] = result.returncode == 0 - except FileNotFoundError: - pass - - return deps - - -if __name__ == "__main__": - # Test the exporter - exporter = PDFExporter() - deps = check_dependencies() - print("Available dependencies:", deps) diff --git a/python/add_creation_time.py b/python/add_creation_time.py deleted file mode 100644 index 135d807..0000000 --- a/python/add_creation_time.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env python3 -""" -Gallery Creation Time Integration -Example function to add creation time metadata to plot items. -This should be integrated into your existing Python gallery generation code. -""" - -from pathlib import Path - - -def add_creation_time_to_items(items, base_path): - """ - Add creation time to plot items for sorting functionality. - - Args: - items: List of plot item dictionaries - base_path: Base path where plot files are located - - Returns: - Updated items list with creation_time field - """ - for item in items: - try: - # Try to get creation time from PNG file first, then PDF - png_path = None - pdf_path = None - - # Extract relative path from href - if 'png_href' in item: - png_rel_path = item['png_href'].replace('../', '').replace( - './', '') - png_path = Path(base_path) / png_rel_path - - if 'pdf_href' in item: - pdf_rel_path = item['pdf_href'].replace('../', '').replace( - './', '') - pdf_path = Path(base_path) / pdf_rel_path - - # Use PNG creation time if available, otherwise PDF - creation_time = 0 - if png_path and png_path.exists(): - creation_time = int(png_path.stat().st_ctime) - elif pdf_path and pdf_path.exists(): - creation_time = int(pdf_path.stat().st_ctime) - - # Add creation time as timestamp (JavaScript can handle this) - item['creation_time'] = creation_time - - except Exception as e: - # Fallback to 0 if there's any error - name = item.get('name', 'unknown') - print(f"Warning: Could not get creation time for {name}: {e}") - item['creation_time'] = 0 - - return items - - -def example_integration(): - """ - Example of how to integrate this into your existing gallery generation. - """ - # This would be part of your existing gallery generation code - items = [ - { - 'name': 'plot1.png', - 'png_href': './plot1.png', - 'pdf_href': './plot1.pdf' - }, - { - 'name': 'plot2.png', - 'png_href': './plot2.png', - 'pdf_href': './plot2.pdf' - } - ] - - base_path = "/path/to/your/gallery/directory" - - # Add creation times - items_with_time = add_creation_time_to_items(items, base_path) - - # Now items_with_time can be passed to your Jinja2 template - # The template will have access to item.creation_time for each item - - return items_with_time - - -if __name__ == "__main__": - # Test the function - items = example_integration() - for item in items: - created = item['creation_time'] - print(f"Plot: {item['name']}, Created: {created}") diff --git a/python/add_metadata.py b/python/add_metadata.py deleted file mode 100644 index cb2d5ef..0000000 --- a/python/add_metadata.py +++ /dev/null @@ -1,45 +0,0 @@ -from typing import Dict, Any -import json -from pathlib import Path - - -def open_metadata(path: str, filename: str = "metadata.json") -> Dict[str, Any]: - """ - Open metadata file and return its contents as a dictionary. - - Args: - path: The directory path where the metadata file is located. - filename: The name of the metadata file (default: "metadata.json"). - - Returns: - A dictionary containing the metadata. - """ - with open(Path(path) / filename, 'r', encoding='utf-8') as f: - try: - return json.load(f) - except json.JSONDecodeError as e: - raise ValueError(f"Error decoding JSON from {filename}: {e}") - except FileNotFoundError: - raise FileNotFoundError(f"Metadata file {filename} not found in {path}") - except Exception as e: - raise RuntimeError(f"Unexpected error reading metadata: {e}") - - -def merge_metadata( - base_metadata: Dict[str, Any], - additional_metadata: Dict[str, Any] -) -> Dict[str, Any]: - """ - Merge two metadata dictionaries. - - Args: - base_metadata: The base metadata dictionary. - additional_metadata: The additional metadata dictionary to merge. - - Returns: - A new dictionary containing the merged metadata. - """ - merged = base_metadata.copy() - for key, value in additional_metadata.items(): - merged[key] = value - return merged diff --git a/templates/gallery.html b/templates/gallery.html deleted file mode 100644 index 35c7ea9..0000000 --- a/templates/gallery.html +++ /dev/null @@ -1,363 +0,0 @@ - - - - - {{ title }} - - - - - - - - -

{{ title }}

- - -
- - 🔍 -
-
- - - - - - - - -
- - - {% if folder_metadata %} - - {% endif %} - - - {% if items %} -
-
- - - - -
-
- - - -
-
- {% endif %} - - -
- {% for item in items %} -
- - {{ item.name }} - -
-
{{ item.name }}
-
- {% if item.creation_time and item.creation_time|int > 0 %} - {{ item.creation_time|int|datetime_from_timestamp|strftime('%Y-%m-%d') }} - {% else %} - Unknown - {% endif %} -
-
-
- {% endfor %} -
- - - {% if subdirs %} -

Subdirectories

- - {% endif %} - - - - - - -
- - - -
- - -
-

Keyboard Shortcuts

-
- Search - Ctrl+K -
-
- Recent plots - Ctrl+R -
-
- Compare plots - Ctrl+C -
-
- Export plots - Ctrl+E -
-
- Exit selection mode - Esc -
-
- Toggle theme - Ctrl+T -
-
- Toggle view - Ctrl+V -
-
- Sort by name - Ctrl+N -
-
- Sort by time - Ctrl+M -
-
- Toggle sort order - Ctrl+O -
-
- Help - ? -
-
- - - - - -
-
-
-

Plot Comparison

- -
-
-
-
- Plot A - -
-
-
-
- 📊 Click to select first plot -
-
-
-
-
-
- Plot B - -
-
-
-
- 📊 Click to select second plot -
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - diff --git a/test.ipynb b/test.ipynb deleted file mode 100644 index 10fe056..0000000 --- a/test.ipynb +++ /dev/null @@ -1,235 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 2, - "id": "aedbb9f5", - "metadata": {}, - "outputs": [], - "source": [ - "from dataclasses import dataclass, field, asdict\n", - "from pathlib import Path\n", - "\n", - "import yaml\n", - "\n", - "\n", - "@dataclass\n", - "class GalleryItem:\n", - " name: str\n", - " path: Path\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "66624faf", - "metadata": {}, - "outputs": [], - "source": [ - "yaml_file = Path(\"config.yaml\")\n", - "\n", - "with open(yaml_file, \"r\") as f:\n", - " new_config: dict = yaml.safe_load(f)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "40a472f8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'name': 'test_1_plot',\n", - " 'path': 'data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1/plot__proc_3_5606769559__cat_incl__var_jet1_pt.pdf'}" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "new_config[\"sources\"][0]" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "745c97ad", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[GalleryItem(name='test_1_plot', path=PosixPath('data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1/plot__proc_3_5606769559__cat_incl__var_jet1_pt.pdf')),\n", - " GalleryItem(name='test_2_dir', path=PosixPath('data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1'))]" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sources = [\n", - " GalleryItem(name=src[\"name\"], path=Path(src[\"path\"]))\n", - " for src in new_config.get(\"sources\", False)\n", - "]\n", - "sources" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "a0102f9d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Config(web_folder='', backup_folder='', png_dpi=400, plot_root='gallery', sources=[{'name': 'test_1_plot', 'path': 'data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1/plot__proc_3_5606769559__cat_incl__var_jet1_pt.pdf'}, {'name': 'test_2_dir', 'path': 'data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1/'}])" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "\n", - "\n", - "@dataclass\n", - "class Config:\n", - " web_folder: str = \"\"\n", - " backup_folder: str = \"\"\n", - " png_dpi: int = 400\n", - " plot_root: str = \"gallery\"\n", - " sources: list[GalleryItem] = field(default_factory=list)\n", - "\n", - " @classmethod\n", - " def from_yaml(cls, yaml_file: str, strict: bool = False) -> \"Config\":\n", - " \"\"\"\n", - " Load configuration from a YAML file.\n", - "\n", - " Args:\n", - " yaml_file (str): Path to the YAML file.\n", - " strict (bool):\n", - " If True, raises an error if a key in the YAML file does not exist in the Config class.\n", - " If False (default), adds all keys as attributes\n", - "\n", - " Returns:\n", - " Config: Instance of this class\n", - " \"\"\"\n", - "\n", - " with open(yaml_file, \"r\") as f:\n", - " new_config: dict = yaml.safe_load(f)\n", - "\n", - " new_config[\"sources\"] = [\n", - " GalleryItem(name=src[\"name\"], path=Path(src[\"path\"]))\n", - " for src in new_config.get(\"sources\", False)\n", - " ]\n", - "\n", - " instance = cls(**{\n", - " k: v\n", - " for k, v in new_config.items()\n", - " if hasattr(cls, k) or not strict\n", - " })\n", - "\n", - " for key in new_config.keys():\n", - " if strict and not hasattr(instance, key):\n", - " raise KeyError(f\"Key '{key}' not found in Config class\")\n", - "\n", - " return instance\n", - "\n", - " def to_yaml(self, yaml_file: str) -> None:\n", - " \"\"\"\n", - " Save the current configuration to a YAML file.\n", - "\n", - " Args:\n", - " yaml_file (str): Path to the YAML file.\n", - " \"\"\"\n", - " with open(yaml_file, \"w\") as f:\n", - " yaml.dump(asdict(self), f, default_flow_style=False)\n", - "\n", - "\n", - "instance = Config(**{\n", - " k: v\n", - " for k, v in new_config.items()\n", - " #if hasattr(Config, k)\n", - "})\n", - "instance" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "0b516ae9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'name': 'test_1_plot',\n", - " 'path': 'data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1/plot__proc_3_5606769559__cat_incl__var_jet1_pt.pdf'},\n", - " {'name': 'test_2_dir',\n", - " 'path': 'data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1/'}]" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "new_config[\"sources\"]" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "b047f0c5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'name': 'test_1_plot',\n", - " 'path': 'data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1/plot__proc_3_5606769559__cat_incl__var_jet1_pt.pdf'},\n", - " {'name': 'test_2_dir',\n", - " 'path': 'data/cf_store/test_analysis/cf.PlotVariables1D/run2_2016_nano_v9/calib__test/sel__test/prod__test/weight__test/nominal/datasets_ttbar_dl/dev1/'}]" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "instance.sources" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/test_logger.py b/test_logger.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_backup.py b/tests/test_backup.py deleted file mode 100644 index 9890cce..0000000 --- a/tests/test_backup.py +++ /dev/null @@ -1,137 +0,0 @@ -import datetime -import zipfile -from unittest.mock import patch - -from gallery.utils.backup import create_backup - - -def test_backup_creates_zip(tmp_path): - web_folder = tmp_path / "plots" - web_folder.mkdir() - (web_folder / "file1.txt").write_text("abc") - (web_folder / "file2.txt").write_text("def") - backup_folder = tmp_path / "backups" - backup_folder.mkdir() - - assert create_backup(web_folder, backup_folder) is True - - today = datetime.date.today().strftime("%Y%m%d") - backup_path = backup_folder / f"backup-{today}.zip" - - assert backup_path.exists() - with zipfile.ZipFile(backup_path, "r") as z: - names = z.namelist() - assert any("file1.txt" in n for n in names) - assert any("file2.txt" in n for n in names) - - -def test_backup_with_subdirectories(tmp_path): - web_folder = tmp_path / "plots" - web_folder.mkdir() - (web_folder / "file1.txt").write_text("content1") - - subdir = web_folder / "subdir" - subdir.mkdir() - (subdir / "file2.txt").write_text("content2") - - nested_subdir = subdir / "nested" - nested_subdir.mkdir() - (nested_subdir / "file3.txt").write_text("content3") - - backup_folder = tmp_path / "backups" - backup_folder.mkdir() - - assert create_backup(web_folder, backup_folder) is True - - today = datetime.date.today().strftime("%Y%m%d") - backup_path = backup_folder / f"backup-{today}.zip" - - assert backup_path.exists() - with zipfile.ZipFile(backup_path, "r") as z: - names = z.namelist() - assert any("file1.txt" in n for n in names) - assert any("file2.txt" in n for n in names) - assert any("file3.txt" in n for n in names) - - -def test_backup_existing_file_is_not_overwritten(tmp_path): - web_folder = tmp_path / "plots" - web_folder.mkdir() - (web_folder / "file1.txt").write_text("abc") - - backup_folder = tmp_path / "backups" - backup_folder.mkdir() - - today = datetime.date.today().strftime("%Y%m%d") - backup_path = backup_folder / f"backup-{today}.zip" - backup_path.write_text("existing backup") - - assert create_backup(web_folder, backup_folder) is True - assert backup_path.read_text() == "existing backup" - - -def test_backup_empty_folder(tmp_path): - web_folder = tmp_path / "plots" - web_folder.mkdir() - - backup_folder = tmp_path / "backups" - backup_folder.mkdir() - - assert create_backup(web_folder, backup_folder) is True - - today = datetime.date.today().strftime("%Y%m%d") - backup_path = backup_folder / f"backup-{today}.zip" - - assert backup_path.exists() - with zipfile.ZipFile(backup_path, "r") as z: - assert len(z.namelist()) == 0 - - -def test_backup_nonexistent_web_folder(tmp_path): - web_folder = tmp_path / "nonexistent_plots" - backup_folder = tmp_path / "backups" - backup_folder.mkdir() - - assert create_backup(web_folder, backup_folder) is True - - today = datetime.date.today().strftime("%Y%m%d") - backup_path = backup_folder / f"backup-{today}.zip" - - assert backup_path.exists() - with zipfile.ZipFile(backup_path, "r") as z: - assert len(z.namelist()) == 0 - - -@patch("datetime.date") -def test_backup_with_custom_date(mock_date, tmp_path): - mock_date.today.return_value.strftime.return_value = "20230908" - - web_folder = tmp_path / "plots" - web_folder.mkdir() - (web_folder / "file1.txt").write_text("test") - - backup_folder = tmp_path / "backups" - backup_folder.mkdir() - - assert create_backup(web_folder, backup_folder) is True - - backup_path = backup_folder / "backup-20230908.zip" - assert backup_path.exists() - - -def test_backup_folder_creation(tmp_path): - web_folder = tmp_path / "plots" - web_folder.mkdir() - (web_folder / "file1.txt").write_text("test") - - # Don't create backup folder - let create_backup() create it - backup_folder = tmp_path / "new_backups" - - assert create_backup(web_folder, backup_folder) is True - - assert backup_folder.exists() - assert backup_folder.is_dir() - - today = datetime.date.today().strftime("%Y%m%d") - backup_path = backup_folder / f"backup-{today}.zip" - assert backup_path.exists() diff --git a/tests/test_config.py b/tests/test_config.py index c776aa0..71b33b9 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -28,7 +28,6 @@ def test_gallery_config_defaults(): assert cfg.plot_root == GalleryDefaults.plot_root assert cfg.cache_enabled == GalleryDefaults.cache_enabled assert cfg.inherit_from_parent == GalleryDefaults.inherit_from_parent - assert cfg.backup_folder == "" def test_gallery_config_sources_from_dicts(): @@ -47,7 +46,7 @@ def test_gallery_config_sources_invalid_type(): def test_gallery_config_from_yaml(tmp_path): yaml_content = { "paths": {"web_folder": "/test/web"}, - "gallery": {"plot_root": "test_plots", "png_dpi": 200, "backup_folder": "test_backups"}, + "gallery": {"plot_root": "test_plots", "png_dpi": 200}, "metadata": {"cache_enabled": False, "inherit_from_parent": False}, "sources": [ {"name": "source1", "path": "/path1"}, @@ -64,7 +63,6 @@ def test_gallery_config_from_yaml(tmp_path): assert cfg.web_folder == Path("/test/web") assert cfg.plot_root == "test_plots" assert cfg.png_dpi == 200 - assert cfg.backup_folder == "test_backups" assert cfg.cache_enabled is False assert cfg.inherit_from_parent is False assert len(cfg.sources) == 2 @@ -106,7 +104,6 @@ def test_gallery_config_to_yaml_round_trip(tmp_path): sources=[{"name": "test", "path": "/test"}], plot_root="plots", png_dpi=300, - backup_folder="backups", ) yaml_file = tmp_path / "output_config.yaml" @@ -118,7 +115,6 @@ def test_gallery_config_to_yaml_round_trip(tmp_path): assert reloaded.web_folder == cfg.web_folder assert reloaded.plot_root == cfg.plot_root assert reloaded.png_dpi == cfg.png_dpi - assert reloaded.backup_folder == cfg.backup_folder assert reloaded.sources[0].name == "test"