From 11bd91c3862438bc7a216c2e3c86c2d56e1890c7 Mon Sep 17 00:00:00 2001 From: jbperrin Date: Fri, 17 Jul 2026 16:22:41 +0200 Subject: [PATCH] fix: mtime filter + lightbox navigation (arrows, swipe, keyboard) --- backend/api/admin_gallery_api.py | 19 ++++-- frontend/templates/admin/gallery.html | 91 ++++++++++++++++++++++----- 2 files changed, 91 insertions(+), 19 deletions(-) diff --git a/backend/api/admin_gallery_api.py b/backend/api/admin_gallery_api.py index 127b30d..96f2179 100644 --- a/backend/api/admin_gallery_api.py +++ b/backend/api/admin_gallery_api.py @@ -66,15 +66,26 @@ async def admin_get_photos( photos = [p for p in all_photos if _is_image(p)] # Enrichit chaque photo avec mtime depuis le disque + # Indexe par stem ET par nom complet pour couvrir les deux formats d'id mtime_index: dict[str, float] = {} - if media_dir.exists(): - for f in media_dir.iterdir(): - mtime_index[f.stem] = f.stat().st_mtime + for media_dir_candidate in [ + media_dir, + Path("/home/pi/photobooth-data/media/processed_full"), + Path("/home/pi/photobooth-data/media"), + ]: + if media_dir_candidate.exists(): + for f in media_dir_candidate.iterdir(): + if f.is_file(): + mt = f.stat().st_mtime + mtime_index[f.name] = mt # uuid.jpg + mtime_index[f.stem] = mt # uuid + break for p in photos: pid = _get_id(p) p["photo_id"] = pid - mt = mtime_index.get(pid, 0.0) + stem = pid.rsplit(".", 1)[0] if "." in pid else pid + mt = mtime_index.get(pid) or mtime_index.get(stem) or 0.0 p["mtime"] = mt p["date_iso"] = datetime.fromtimestamp(mt).strftime("%Y-%m-%d") if mt else "" p["date_label"] = datetime.fromtimestamp(mt).strftime("%d/%m/%Y %H:%M") if mt else "" diff --git a/frontend/templates/admin/gallery.html b/frontend/templates/admin/gallery.html index 4406251..9198aa6 100644 --- a/frontend/templates/admin/gallery.html +++ b/frontend/templates/admin/gallery.html @@ -106,16 +106,31 @@ display: none; position: fixed; inset: 0; z-index: 1000; background: rgba(0,0,0,.92); backdrop-filter: blur(8px); align-items: center; justify-content: center; padding: 1.5rem; + gap: 1rem; } .lightbox.open { display: flex; } +.lb-nav-btn { + flex-shrink: 0; width: 44px; height: 44px; border-radius: 50%; + border: 1px solid rgba(255,255,255,.2); background: rgba(255,255,255,.08); + color: #fff; font-size: 1.3rem; cursor: pointer; display: flex; + align-items: center; justify-content: center; transition: background .15s; + user-select: none; +} +.lb-nav-btn:hover:not(:disabled) { background: rgba(255,255,255,.18); } +.lb-nav-btn:disabled { opacity: .2; cursor: default; } + .lb-modal { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; overflow: hidden; - max-width: min(900px, 95vw); width: 100%; + max-width: min(860px, calc(95vw - 110px)); width: 100%; display: flex; flex-direction: column; max-height: 95vh; } +.lb-counter { + font-size: .75rem; color: var(--text-muted); white-space: nowrap; +} + .lb-header { display: flex; align-items: center; justify-content: space-between; padding: .75rem 1.1rem; border-bottom: 1px solid var(--border); @@ -247,6 +262,7 @@ @@ -294,6 +311,7 @@ + {% endblock %} @@ -302,6 +320,8 @@ let currentPage = 1; let totalPages = 1; let allPhotos = []; +let displayedPhotos = []; // photos actuellement dans la grille (après filtre statut) +let currentIndex = -1; // index dans displayedPhotos let currentPhotoId = null; let currentPhotoData = null; @@ -411,15 +431,17 @@ function updateFilterSummary(total) { // Rendu grille // ════════════════════════════════════════════════════════════════════════════ function renderGrid(photos) { + displayedPhotos = photos || []; const grid = document.getElementById('photo-grid'); - if (!photos || !photos.length) { + if (!displayedPhotos.length) { grid.innerHTML = `
📷 Aucune photo pour ces filtres
`; return; } - grid.innerHTML = photos.map(p => { + // On utilise l'index (pas le JSON sérialisé) pour éviter les problèmes de quotes + grid.innerHTML = displayedPhotos.map((p, idx) => { const pid = p.photo_id || p.id || ''; const hasPending = p.print_pending > 0; const hasPrinting = p.print_printing > 0; @@ -433,7 +455,7 @@ function renderGrid(photos) { return `
+ onclick="openLightboxIdx(${idx})"> ${badge}${dateLabel}
@@ -450,19 +472,31 @@ function renderGrid(photos) { // ════════════════════════════════════════════════════════════════════════════ // Lightbox // ════════════════════════════════════════════════════════════════════════════ -function openLightbox(photo) { +function openLightboxIdx(idx) { + if (idx < 0 || idx >= displayedPhotos.length) return; + currentIndex = idx; + const photo = displayedPhotos[idx]; currentPhotoData = photo; currentPhotoId = photo.photo_id || photo.id || ''; - document.getElementById('lb-img').src = photo.full_url || photo.thumb_url; - document.getElementById('lb-date').textContent = photo.date_label || '—'; - document.getElementById('lb-id').textContent = currentPhotoId; - document.getElementById('lb-btn-dl').href = photo.download_url || photo.full_url; + document.getElementById('lb-img').src = photo.full_url || photo.thumb_url; + document.getElementById('lb-date').textContent = photo.date_label || '—'; + document.getElementById('lb-id').textContent = currentPhotoId; + document.getElementById('lb-btn-dl').href = photo.download_url || photo.full_url; + document.getElementById('lb-counter').textContent = `${idx + 1} / ${displayedPhotos.length}`; + + // Boutons nav + document.getElementById('lb-prev').disabled = idx <= 0; + document.getElementById('lb-next').disabled = idx >= displayedPhotos.length - 1; _updateLbPrintPanel(photo); document.getElementById('lightbox').classList.add('open'); } +function lbNavigate(dir) { + openLightboxIdx(currentIndex + dir); +} + function _updateLbPrintPanel(photo) { const panel = document.getElementById('lb-print-panel'); const list = document.getElementById('lb-queue-list'); @@ -496,12 +530,31 @@ function closeLightbox() { document.getElementById('lightbox').classList.remove('open'); document.getElementById('lb-img').src = ''; currentPhotoId = currentPhotoData = null; + currentIndex = -1; } function lbBackdropClose(e) { if (e.target === document.getElementById('lightbox')) closeLightbox(); } -document.addEventListener('keydown', e => { if (e.key === 'Escape') closeLightbox(); }); + +// Clavier : Échap + flèches +document.addEventListener('keydown', e => { + const lb = document.getElementById('lightbox'); + if (!lb.classList.contains('open')) return; + if (e.key === 'Escape') { closeLightbox(); } + else if (e.key === 'ArrowLeft') { lbNavigate(-1); } + else if (e.key === 'ArrowRight') { lbNavigate(1); } +}); + +// Swipe tactile +let _touchX = 0; +document.getElementById('lightbox').addEventListener('touchstart', e => { + _touchX = e.touches[0].clientX; +}, { passive: true }); +document.getElementById('lightbox').addEventListener('touchend', e => { + const dx = e.changedTouches[0].clientX - _touchX; + if (Math.abs(dx) > 50) lbNavigate(dx < 0 ? 1 : -1); +}, { passive: true }); // ════════════════════════════════════════════════════════════════════════════ // Actions impression @@ -586,15 +639,23 @@ async function _refreshPhotoData(pid) { try { const data = await api('GET', `/admin/api/gallery/photos?${_buildParams(currentPage)}`); allPhotos = data.photos || []; - const photo = allPhotos.find(p => (p.photo_id || p.id) === pid); - if (photo && currentPhotoId === pid) { - currentPhotoData = photo; - _updateLbPrintPanel(photo); - } let photos = allPhotos; if (statusFilter === 'pending') photos = allPhotos.filter(p => p.print_pending > 0 || p.print_printing > 0); renderGrid(photos); document.getElementById('pending-count').textContent = allPhotos.filter(p => p.print_pending > 0).length; + + // Si lightbox ouverte sur cette photo, mettre à jour le panel impression + if (currentPhotoId === pid) { + const newIdx = displayedPhotos.findIndex(p => (p.photo_id || p.id) === pid); + if (newIdx >= 0) { + currentIndex = newIdx; + currentPhotoData = displayedPhotos[newIdx]; + _updateLbPrintPanel(currentPhotoData); + document.getElementById('lb-prev').disabled = newIdx <= 0; + document.getElementById('lb-next').disabled = newIdx >= displayedPhotos.length - 1; + document.getElementById('lb-counter').textContent = `${newIdx + 1} / ${displayedPhotos.length}`; + } + } } catch(e) {} }