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) {}
}