Admin galerie + compteur copies + flash capture + erreur imprimante toast

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 13:24:55 +02:00
parent e9143f7a72
commit 8bc854b5a7
6 changed files with 139 additions and 12 deletions

View File

@@ -11,7 +11,7 @@ from backend.config import charger_config, mettre_a_jour_config
log = logging.getLogger("photobooth.destinations") 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.""" """Copie la photo vers toutes les destinations activees."""
config = charger_config() config = charger_config()
dest = config.get("destinations", {}) dest = config.get("destinations", {})
@@ -37,11 +37,11 @@ def distribuer_photo(chemin_photo: Path, imprimee: bool = False):
if dest.get("ftp", False): if dest.get("ftp", False):
envoyer_ftp(chemin_photo, dest, sous_dossier) envoyer_ftp(chemin_photo, dest, sous_dossier)
# Incrementer le compteur (photos imprimees seulement) # Incrementer le compteur (feuilles 10x15 consommees)
if imprimee: if imprimee:
compteur = config.get("compteur", {}) compteur = config.get("compteur", {})
if compteur.get("actif", False): 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}) mettre_a_jour_config({"compteur": compteur})

View File

@@ -903,7 +903,7 @@ async def api_imprimer(donnees: dict):
format_papier = donnees.get("format_papier") or None format_papier = donnees.get("format_papier") or None
resultat = imprimer(chemin, copies=copies, cadre_override=cadre_override, format_papier=format_papier) resultat = imprimer(chemin, copies=copies, cadre_override=cadre_override, format_papier=format_papier)
if resultat.get("succes"): if resultat.get("succes"):
distribuer_photo(chemin, imprimee=True) distribuer_photo(chemin, imprimee=True, copies=copies)
return resultat return resultat

View File

@@ -413,6 +413,7 @@ html, body {
@keyframes flash { @keyframes flash {
0% { opacity: 1; } 0% { opacity: 1; }
30% { opacity: 0.9; }
100% { opacity: 0; } 100% { opacity: 0; }
} }
@@ -1772,18 +1773,33 @@ h3 {
border-radius: 3px; 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 === */ /* === Erreur camera === */
.printer-erreur { .printer-erreur {
position: fixed; position: fixed;
bottom: 0; left: 0; right: 0; top: 1rem; right: 1rem;
background: #b71c1c; background: rgba(183, 28, 28, 0.85);
color: #fff; color: #fff;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 0.5rem;
padding: 0.8rem 1.5rem; padding: 0.5rem 1rem;
z-index: 9999; z-index: 100;
font-size: 1rem; font-size: 0.85rem;
border-radius: 10px;
max-width: 300px;
} }
.printer-erreur-icon { font-size: 1.4rem; flex-shrink: 0; } .printer-erreur-icon { font-size: 1.4rem; flex-shrink: 0; }

View File

@@ -378,6 +378,7 @@
<button class="onglet" data-onglet="personnalisation">Personnalisation</button> <button class="onglet" data-onglet="personnalisation">Personnalisation</button>
<button class="onglet" data-onglet="evenement">Evenement</button> <button class="onglet" data-onglet="evenement">Evenement</button>
<button class="onglet" data-onglet="fonctions">Fonctions</button> <button class="onglet" data-onglet="fonctions">Fonctions</button>
<button class="onglet" data-onglet="admin-galerie" onclick="chargerAdminGalerie()">Galerie</button>
<button class="onglet" data-onglet="eclairage" onclick="demarrerEclairageLive()">Eclairage</button> <button class="onglet" data-onglet="eclairage" onclick="demarrerEclairageLive()">Eclairage</button>
<button class="onglet" data-onglet="wifi" onclick="chargerWifi()">WiFi</button> <button class="onglet" data-onglet="wifi" onclick="chargerWifi()">WiFi</button>
<button class="onglet" data-onglet="infos" onclick="chargerInfosSysteme()">Infos</button> <button class="onglet" data-onglet="infos" onclick="chargerInfosSysteme()">Infos</button>
@@ -803,7 +804,20 @@
</div> </div>
</div> </div>
<!-- Panneau Infos systeme --> <!-- Panneau Galerie admin -->
<div class="admin-panneau" id="panneau-admin-galerie">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;flex-wrap:wrap;gap:0.5rem">
<h3 style="margin:0">Photos</h3>
<div style="display:flex;gap:0.5rem;align-items:center">
<span id="admin-galerie-selection" style="font-size:0.85rem;color:#aaa"></span>
<button class="btn-action btn-petit" onclick="adminGalerieSelectAll()">Tout selectionner</button>
<button class="btn-action btn-petit" onclick="adminGalerieDeselectAll()">Deselectionner</button>
<button class="btn-secondaire btn-petit" onclick="adminGalerieImprimer()">Imprimer selection</button>
</div>
</div>
<div id="admin-galerie-grille" class="grille-galerie" style="max-height:60vh;overflow-y:auto"></div>
</div>
<div class="admin-panneau" id="panneau-eclairage"> <div class="admin-panneau" id="panneau-eclairage">
<div style="display:flex;gap:1rem;align-items:flex-start;flex-wrap:wrap"> <div style="display:flex;gap:1rem;align-items:flex-start;flex-wrap:wrap">
<div style="flex:1;min-width:280px"> <div style="flex:1;min-width:280px">

View File

@@ -252,10 +252,18 @@ async function lancerCapture() {
lancerPreview(); // Miroir live pendant le compte à rebours lancerPreview(); // Miroir live pendant le compte à rebours
await compteARebours(); 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); const promesseCapture = apiPost('/api/capturer').catch(() => null);
arreterPreview();
let resultat = await promesseCapture; let resultat = await promesseCapture;
flash.classList.add('cache');
document.getElementById('capture-en-cours').classList.add('cache'); document.getElementById('capture-en-cours').classList.add('cache');
let erreurReseau = false; let erreurReseau = false;

View File

@@ -25,3 +25,92 @@ function voirPhoto(photo) {
document.getElementById('photo-partage').src = '/' + photo.chemin; document.getElementById('photo-partage').src = '/' + photo.chemin;
allerA('partage'); 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 = '<p style="color:var(--texte-secondaire);grid-column:1/-1;text-align:center">Aucune photo</p>';
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' : ''));
}