fix: mtime filter + lightbox navigation (arrows, swipe, keyboard)
🚀 Deploy — JH Photomaton / 🔍 Vérification (push) Has been cancelled
🚀 Deploy — JH Photomaton / 🍓 Deploy sur le Pi (push) Has been cancelled

This commit is contained in:
2026-07-17 16:22:41 +02:00
parent c89924ea18
commit 11bd91c386
2 changed files with 91 additions and 19 deletions
+15 -4
View File
@@ -66,15 +66,26 @@ async def admin_get_photos(
photos = [p for p in all_photos if _is_image(p)] photos = [p for p in all_photos if _is_image(p)]
# Enrichit chaque photo avec mtime depuis le disque # 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] = {} mtime_index: dict[str, float] = {}
if media_dir.exists(): for media_dir_candidate in [
for f in media_dir.iterdir(): media_dir,
mtime_index[f.stem] = f.stat().st_mtime 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: for p in photos:
pid = _get_id(p) pid = _get_id(p)
p["photo_id"] = pid 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["mtime"] = mt
p["date_iso"] = datetime.fromtimestamp(mt).strftime("%Y-%m-%d") if mt else "" 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 "" p["date_label"] = datetime.fromtimestamp(mt).strftime("%d/%m/%Y %H:%M") if mt else ""
+72 -11
View File
@@ -106,16 +106,31 @@
display: none; position: fixed; inset: 0; z-index: 1000; display: none; position: fixed; inset: 0; z-index: 1000;
background: rgba(0,0,0,.92); backdrop-filter: blur(8px); background: rgba(0,0,0,.92); backdrop-filter: blur(8px);
align-items: center; justify-content: center; padding: 1.5rem; align-items: center; justify-content: center; padding: 1.5rem;
gap: 1rem;
} }
.lightbox.open { display: flex; } .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 { .lb-modal {
background: var(--surface); border: 1px solid var(--border); background: var(--surface); border: 1px solid var(--border);
border-radius: 16px; overflow: hidden; 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; display: flex; flex-direction: column; max-height: 95vh;
} }
.lb-counter {
font-size: .75rem; color: var(--text-muted); white-space: nowrap;
}
.lb-header { .lb-header {
display: flex; align-items: center; justify-content: space-between; display: flex; align-items: center; justify-content: space-between;
padding: .75rem 1.1rem; border-bottom: 1px solid var(--border); padding: .75rem 1.1rem; border-bottom: 1px solid var(--border);
@@ -247,6 +262,7 @@
<!-- ── Lightbox ─────────────────────────────────────────────────────────────── --> <!-- ── Lightbox ─────────────────────────────────────────────────────────────── -->
<div class="lightbox" id="lightbox" onclick="lbBackdropClose(event)"> <div class="lightbox" id="lightbox" onclick="lbBackdropClose(event)">
<button class="lb-nav-btn" id="lb-prev" onclick="lbNavigate(-1);event.stopPropagation()"></button>
<div class="lb-modal"> <div class="lb-modal">
<!-- En-tête --> <!-- En-tête -->
@@ -255,6 +271,7 @@
<div class="lb-date" id="lb-date"></div> <div class="lb-date" id="lb-date"></div>
<div class="lb-id" id="lb-id"></div> <div class="lb-id" id="lb-id"></div>
</div> </div>
<span class="lb-counter" id="lb-counter"></span>
<button class="lb-close-btn" onclick="closeLightbox()"></button> <button class="lb-close-btn" onclick="closeLightbox()"></button>
</div> </div>
@@ -294,6 +311,7 @@
</div> </div>
</div> </div>
<button class="lb-nav-btn" id="lb-next" onclick="lbNavigate(1);event.stopPropagation()"></button>
</div> </div>
{% endblock %} {% endblock %}
@@ -302,6 +320,8 @@
let currentPage = 1; let currentPage = 1;
let totalPages = 1; let totalPages = 1;
let allPhotos = []; let allPhotos = [];
let displayedPhotos = []; // photos actuellement dans la grille (après filtre statut)
let currentIndex = -1; // index dans displayedPhotos
let currentPhotoId = null; let currentPhotoId = null;
let currentPhotoData = null; let currentPhotoData = null;
@@ -411,15 +431,17 @@ function updateFilterSummary(total) {
// Rendu grille // Rendu grille
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
function renderGrid(photos) { function renderGrid(photos) {
displayedPhotos = photos || [];
const grid = document.getElementById('photo-grid'); const grid = document.getElementById('photo-grid');
if (!photos || !photos.length) { if (!displayedPhotos.length) {
grid.innerHTML = `<div style="color:var(--text-muted);grid-column:1/-1;text-align:center;padding:2rem"> grid.innerHTML = `<div style="color:var(--text-muted);grid-column:1/-1;text-align:center;padding:2rem">
📷 Aucune photo pour ces filtres</div>`; 📷 Aucune photo pour ces filtres</div>`;
return; 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 pid = p.photo_id || p.id || '';
const hasPending = p.print_pending > 0; const hasPending = p.print_pending > 0;
const hasPrinting = p.print_printing > 0; const hasPrinting = p.print_printing > 0;
@@ -433,7 +455,7 @@ function renderGrid(photos) {
return ` return `
<div class="photo-card ${hasPending || hasPrinting ? 'has-print' : ''}" id="card-${pid}" <div class="photo-card ${hasPending || hasPrinting ? 'has-print' : ''}" id="card-${pid}"
onclick="openLightbox(${JSON.stringify(p).replace(/'/g, '&#39;')})"> onclick="openLightboxIdx(${idx})">
<img src="${p.thumb_url}" loading="lazy" alt=""> <img src="${p.thumb_url}" loading="lazy" alt="">
${badge}${dateLabel} ${badge}${dateLabel}
<div class="card-overlay"> <div class="card-overlay">
@@ -450,7 +472,10 @@ function renderGrid(photos) {
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
// Lightbox // Lightbox
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
function openLightbox(photo) { function openLightboxIdx(idx) {
if (idx < 0 || idx >= displayedPhotos.length) return;
currentIndex = idx;
const photo = displayedPhotos[idx];
currentPhotoData = photo; currentPhotoData = photo;
currentPhotoId = photo.photo_id || photo.id || ''; currentPhotoId = photo.photo_id || photo.id || '';
@@ -458,11 +483,20 @@ function openLightbox(photo) {
document.getElementById('lb-date').textContent = photo.date_label || '—'; document.getElementById('lb-date').textContent = photo.date_label || '—';
document.getElementById('lb-id').textContent = currentPhotoId; document.getElementById('lb-id').textContent = currentPhotoId;
document.getElementById('lb-btn-dl').href = photo.download_url || photo.full_url; 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); _updateLbPrintPanel(photo);
document.getElementById('lightbox').classList.add('open'); document.getElementById('lightbox').classList.add('open');
} }
function lbNavigate(dir) {
openLightboxIdx(currentIndex + dir);
}
function _updateLbPrintPanel(photo) { function _updateLbPrintPanel(photo) {
const panel = document.getElementById('lb-print-panel'); const panel = document.getElementById('lb-print-panel');
const list = document.getElementById('lb-queue-list'); const list = document.getElementById('lb-queue-list');
@@ -496,12 +530,31 @@ function closeLightbox() {
document.getElementById('lightbox').classList.remove('open'); document.getElementById('lightbox').classList.remove('open');
document.getElementById('lb-img').src = ''; document.getElementById('lb-img').src = '';
currentPhotoId = currentPhotoData = null; currentPhotoId = currentPhotoData = null;
currentIndex = -1;
} }
function lbBackdropClose(e) { function lbBackdropClose(e) {
if (e.target === document.getElementById('lightbox')) closeLightbox(); 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 // Actions impression
@@ -586,15 +639,23 @@ async function _refreshPhotoData(pid) {
try { try {
const data = await api('GET', `/admin/api/gallery/photos?${_buildParams(currentPage)}`); const data = await api('GET', `/admin/api/gallery/photos?${_buildParams(currentPage)}`);
allPhotos = data.photos || []; 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; let photos = allPhotos;
if (statusFilter === 'pending') photos = allPhotos.filter(p => p.print_pending > 0 || p.print_printing > 0); if (statusFilter === 'pending') photos = allPhotos.filter(p => p.print_pending > 0 || p.print_printing > 0);
renderGrid(photos); renderGrid(photos);
document.getElementById('pending-count').textContent = allPhotos.filter(p => p.print_pending > 0).length; 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) {} } catch(e) {}
} }