From 8bc854b5a77b810a66eadafdaf75d62a98a9e535 Mon Sep 17 00:00:00 2001 From: Jules Date: Tue, 30 Jun 2026 13:24:55 +0200 Subject: [PATCH] Admin galerie + compteur copies + flash capture + erreur imprimante toast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Galerie admin : onglet avec sélection photos + impression par lot - Compteur : compte les copies (×N) et non plus 1 par impression - Flash blanc immédiat après countdown pour masquer le délai AF - Erreur imprimante : petit toast discret au lieu de bannière plein écran Co-Authored-By: Claude Opus 4.6 --- backend/destinations.py | 6 +-- backend/main.py | 2 +- frontend/css/style.css | 28 ++++++++++--- frontend/index.html | 16 +++++++- frontend/js/camera.js | 10 ++++- frontend/js/gallery.js | 89 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 139 insertions(+), 12 deletions(-) diff --git a/backend/destinations.py b/backend/destinations.py index 8eb519b..1a1775c 100644 --- a/backend/destinations.py +++ b/backend/destinations.py @@ -11,7 +11,7 @@ from backend.config import charger_config, mettre_a_jour_config log = logging.getLogger("photobooth.destinations") -def distribuer_photo(chemin_photo: Path, imprimee: bool = False): +def distribuer_photo(chemin_photo: Path, imprimee: bool = False, copies: int = 1): """Copie la photo vers toutes les destinations activees.""" config = charger_config() dest = config.get("destinations", {}) @@ -37,11 +37,11 @@ def distribuer_photo(chemin_photo: Path, imprimee: bool = False): if dest.get("ftp", False): envoyer_ftp(chemin_photo, dest, sous_dossier) - # Incrementer le compteur (photos imprimees seulement) + # Incrementer le compteur (feuilles 10x15 consommees) if imprimee: compteur = config.get("compteur", {}) if compteur.get("actif", False): - compteur["photos_prises"] = compteur.get("photos_prises", 0) + 1 + compteur["photos_prises"] = compteur.get("photos_prises", 0) + copies mettre_a_jour_config({"compteur": compteur}) diff --git a/backend/main.py b/backend/main.py index 8a45088..daa8b27 100644 --- a/backend/main.py +++ b/backend/main.py @@ -903,7 +903,7 @@ async def api_imprimer(donnees: dict): format_papier = donnees.get("format_papier") or None resultat = imprimer(chemin, copies=copies, cadre_override=cadre_override, format_papier=format_papier) if resultat.get("succes"): - distribuer_photo(chemin, imprimee=True) + distribuer_photo(chemin, imprimee=True, copies=copies) return resultat diff --git a/frontend/css/style.css b/frontend/css/style.css index 831c106..b3229d1 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -413,6 +413,7 @@ html, body { @keyframes flash { 0% { opacity: 1; } + 30% { opacity: 0.9; } 100% { opacity: 0; } } @@ -1772,18 +1773,33 @@ h3 { border-radius: 3px; } +/* === Admin galerie check === */ +.admin-galerie-check { + position: absolute; top: 6px; right: 6px; + width: 28px; height: 28px; border-radius: 50%; + background: rgba(0,0,0,0.4); border: 2px solid #fff; + color: #fff; font-size: 1rem; display: flex; + align-items: center; justify-content: center; + opacity: 0; transition: opacity 0.15s; +} +.admin-galerie-check.visible { + opacity: 1; background: var(--accent); +} + /* === Erreur camera === */ .printer-erreur { position: fixed; - bottom: 0; left: 0; right: 0; - background: #b71c1c; + top: 1rem; right: 1rem; + background: rgba(183, 28, 28, 0.85); color: #fff; display: flex; align-items: center; - gap: 1rem; - padding: 0.8rem 1.5rem; - z-index: 9999; - font-size: 1rem; + gap: 0.5rem; + padding: 0.5rem 1rem; + z-index: 100; + font-size: 0.85rem; + border-radius: 10px; + max-width: 300px; } .printer-erreur-icon { font-size: 1.4rem; flex-shrink: 0; } diff --git a/frontend/index.html b/frontend/index.html index d2b0913..1834e8c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -378,6 +378,7 @@ + @@ -803,7 +804,20 @@ - + +
+
+

Photos

+
+ + + + +
+
+
+
+
diff --git a/frontend/js/camera.js b/frontend/js/camera.js index f1542af..2f72214 100644 --- a/frontend/js/camera.js +++ b/frontend/js/camera.js @@ -252,10 +252,18 @@ async function lancerCapture() { lancerPreview(); // Miroir live pendant le compte à rebours await compteARebours(); - arreterPreview(); + + // Flash blanc immédiat + lancer capture en parallèle + const flash = document.getElementById('flash-blanc'); + flash.classList.remove('cache'); + flash.style.animation = 'none'; + flash.offsetHeight; + flash.style.animation = ''; const promesseCapture = apiPost('/api/capturer').catch(() => null); + arreterPreview(); let resultat = await promesseCapture; + flash.classList.add('cache'); document.getElementById('capture-en-cours').classList.add('cache'); let erreurReseau = false; diff --git a/frontend/js/gallery.js b/frontend/js/gallery.js index f491a11..bd00e8d 100644 --- a/frontend/js/gallery.js +++ b/frontend/js/gallery.js @@ -25,3 +25,92 @@ function voirPhoto(photo) { document.getElementById('photo-partage').src = '/' + photo.chemin; allerA('partage'); } + +/* --- Admin gallery --- */ + +let adminGaleriePhotos = []; +let adminGalerieSelection = new Set(); + +async function chargerAdminGalerie() { + const photos = await apiGet('/api/galerie'); + adminGaleriePhotos = photos; + adminGalerieSelection.clear(); + const grille = document.getElementById('admin-galerie-grille'); + grille.innerHTML = ''; + + if (photos.length === 0) { + grille.innerHTML = '

Aucune photo

'; + majAdminGalerieSelection(); + return; + } + + for (const photo of photos) { + const wrap = document.createElement('div'); + wrap.style.cssText = 'position:relative;cursor:pointer'; + wrap.dataset.nom = photo.nom; + + const img = document.createElement('img'); + img.src = '/' + photo.chemin; + img.alt = photo.nom; + img.loading = 'lazy'; + img.style.cssText = 'width:100%;border-radius:8px;display:block'; + + const check = document.createElement('div'); + check.className = 'admin-galerie-check'; + check.textContent = '✓'; + + wrap.appendChild(img); + wrap.appendChild(check); + wrap.addEventListener('click', () => toggleAdminGaleriePhoto(photo.nom, wrap)); + grille.appendChild(wrap); + } + majAdminGalerieSelection(); +} + +function toggleAdminGaleriePhoto(nom, wrap) { + if (adminGalerieSelection.has(nom)) { + adminGalerieSelection.delete(nom); + wrap.querySelector('.admin-galerie-check').classList.remove('visible'); + } else { + adminGalerieSelection.add(nom); + wrap.querySelector('.admin-galerie-check').classList.add('visible'); + } + majAdminGalerieSelection(); +} + +function adminGalerieSelectAll() { + const grille = document.getElementById('admin-galerie-grille'); + for (const photo of adminGaleriePhotos) { + adminGalerieSelection.add(photo.nom); + } + grille.querySelectorAll('.admin-galerie-check').forEach(c => c.classList.add('visible')); + majAdminGalerieSelection(); +} + +function adminGalerieDeselectAll() { + adminGalerieSelection.clear(); + const grille = document.getElementById('admin-galerie-grille'); + grille.querySelectorAll('.admin-galerie-check').forEach(c => c.classList.remove('visible')); + majAdminGalerieSelection(); +} + +function majAdminGalerieSelection() { + const span = document.getElementById('admin-galerie-selection'); + const n = adminGalerieSelection.size; + span.textContent = n > 0 ? n + ' selectionnee' + (n > 1 ? 's' : '') : ''; +} + +async function adminGalerieImprimer() { + const noms = [...adminGalerieSelection]; + if (noms.length === 0) return; + if (!confirm('Imprimer ' + noms.length + ' photo' + (noms.length > 1 ? 's' : '') + ' ?')) return; + + let ok = 0, fail = 0; + for (const nom of noms) { + try { + const r = await apiPost('/api/imprimer', { photo: nom }); + if (r && r.succes) ok++; else fail++; + } catch { fail++; } + } + alert('Impression : ' + ok + ' OK' + (fail > 0 ? ', ' + fail + ' echec' : '')); +}