diff --git a/backend/destinations.py b/backend/destinations.py new file mode 100644 index 0000000..6c8cf38 --- /dev/null +++ b/backend/destinations.py @@ -0,0 +1,114 @@ +import ftplib +import logging +import shutil +from pathlib import Path + +from backend.config import charger_config, mettre_a_jour_config + +log = logging.getLogger("photobooth.destinations") + + +def distribuer_photo(chemin_photo: Path, imprimee: bool = False): + """Copie la photo vers toutes les destinations activees.""" + config = charger_config() + dest = config.get("destinations", {}) + + # Verifier si on sauvegarde tout ou seulement les imprimees + if not dest.get("sauvegarder_tout", True) and not imprimee: + log.info(f"Photo non imprimee, pas de distribution : {chemin_photo.name}") + return + + # Cle USB + if dest.get("cle_usb", False): + copier_usb(chemin_photo, dest.get("chemin_usb", "/media/usb")) + + # FTP + if dest.get("ftp", False): + envoyer_ftp(chemin_photo, dest) + + # Incrementer le compteur + compteur = config.get("compteur", {}) + if compteur.get("actif", False): + compteur["photos_prises"] = compteur.get("photos_prises", 0) + 1 + mettre_a_jour_config({"compteur": compteur}) + + +def copier_usb(chemin_photo: Path, chemin_usb: str): + """Copie une photo sur la cle USB.""" + dossier_usb = Path(chemin_usb) + if not dossier_usb.exists(): + log.warning(f"Cle USB non trouvee : {chemin_usb}") + return False + + dossier_dest = dossier_usb / "photobooth" + dossier_dest.mkdir(exist_ok=True) + + try: + shutil.copy2(chemin_photo, dossier_dest / chemin_photo.name) + log.info(f"Photo copiee sur USB : {chemin_photo.name}") + return True + except OSError as e: + log.error(f"Erreur copie USB : {e}") + return False + + +def envoyer_ftp(chemin_photo: Path, config_dest: dict): + """Envoie une photo par FTP.""" + host = config_dest.get("ftp_host", "") + port = config_dest.get("ftp_port", 21) + user = config_dest.get("ftp_user", "") + password = config_dest.get("ftp_password", "") + chemin_distant = config_dest.get("ftp_chemin", "/photobooth") + + if not host: + log.warning("FTP non configure") + return False + + try: + with ftplib.FTP() as ftp: + ftp.connect(host, port, timeout=10) + ftp.login(user, password) + # Creer le dossier si necessaire + try: + ftp.mkd(chemin_distant) + except ftplib.error_perm: + pass + ftp.cwd(chemin_distant) + with open(chemin_photo, "rb") as f: + ftp.storbinary(f"STOR {chemin_photo.name}", f) + log.info(f"Photo envoyee par FTP : {chemin_photo.name}") + return True + except (ftplib.all_errors, OSError) as e: + log.error(f"Erreur FTP : {e}") + return False + + +def detecter_usb() -> list[str]: + """Detecte les cles USB montees.""" + chemins_possibles = [Path("/media"), Path("/mnt")] + usb_trouvees = [] + for racine in chemins_possibles: + if racine.exists(): + for sous_dossier in racine.iterdir(): + if sous_dossier.is_mount() or (sous_dossier.is_dir() and any(sous_dossier.iterdir())): + usb_trouvees.append(str(sous_dossier)) + return usb_trouvees + + +def compteur_restant() -> dict: + """Retourne l'etat du compteur.""" + config = charger_config() + compteur = config.get("compteur", {}) + limite = compteur.get("limite", 400) + prises = compteur.get("photos_prises", 0) + return { + "actif": compteur.get("actif", False), + "limite": limite, + "photos_prises": prises, + "restantes": max(0, limite - prises), + } + + +def reset_compteur(): + """Remet le compteur a zero.""" + mettre_a_jour_config({"compteur": {"photos_prises": 0}}) diff --git a/backend/main.py b/backend/main.py index 0b35edc..e348923 100644 --- a/backend/main.py +++ b/backend/main.py @@ -17,7 +17,7 @@ from backend.camera import camera from backend.gallery import lister_photos, compter_photos, supprimer_photo, vider_galerie from backend.effects import appliquer_filtre, appliquer_overlay, chroma_key, lister_overlays, lister_fonds, FILTRES from backend.collage import creer_strip, creer_collage - +from backend.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur from backend.printer import lister_imprimantes, imprimer from backend.mailer import envoyer_photo from backend.qrcode_gen import generer_qr, qr_galerie @@ -73,10 +73,19 @@ async def api_config_update(modifications: dict): @app.post("/api/capturer") async def api_capturer(): + # Verifier le compteur + etat = compteur_restant() + if etat["actif"] and etat["restantes"] <= 0: + return JSONResponse({"erreur": "Limite de photos atteinte"}, status_code=403) + chemin = camera.capturer() if chemin is None: return JSONResponse({"erreur": "Echec capture"}, status_code=500) nom = chemin.name + + # Distribuer vers les destinations + distribuer_photo(chemin, imprimee=False) + await diffuser_ws({"type": "photo_capturee", "nom": nom}) return {"nom": nom, "chemin": f"/data/photos/{nom}"} @@ -206,6 +215,8 @@ async def api_imprimer(donnees: dict): if not chemin.exists(): return JSONResponse({"erreur": "Photo introuvable"}, status_code=404) ok = imprimer(chemin) + if ok: + distribuer_photo(chemin, imprimee=True) return {"succes": ok} @@ -278,6 +289,60 @@ async def api_upload_fond(fichier: UploadFile = File(...)): return {"nom": fichier.filename} +# --- API Compteur --- + +@app.get("/api/compteur") +async def api_compteur_etat(): + return compteur_restant() + + +@app.post("/api/compteur/reset") +async def api_compteur_reset(): + reset_compteur() + return compteur_restant() + + +# --- API Destinations --- + +@app.get("/api/usb/detecter") +async def api_detecter_usb(): + return detecter_usb() + + +# --- API Cadres actifs --- + +@app.get("/api/cadres") +async def api_cadres(): + config = charger_config() + tous = lister_overlays() + actifs = config.get("cadres", {}).get("actifs", []) + return {"tous": tous, "actifs": actifs} + + +@app.post("/api/cadres") +async def api_cadres_update(donnees: dict): + actifs = donnees.get("actifs", []) + mettre_a_jour_config({"cadres": {"actifs": actifs}}) + return {"actifs": actifs} + + +# --- API Animations compte a rebours --- + +ANIMATIONS_CAR = { + "classique": "Classique (chiffres simples)", + "explosion": "Explosion (chiffres qui eclatent)", + "rebond": "Rebond (chiffres qui rebondissent)", + "rotation": "Rotation 3D", + "fondu": "Fondu enchaine", + "emoji": "Emoji fun", + "shake": "Tremblement", +} + +@app.get("/api/animations") +async def api_animations(): + return ANIMATIONS_CAR + + # --- API Systeme --- @app.post("/api/systeme/redemarrer") diff --git a/data/config.json b/data/config.json index f8ce24e..57266a3 100644 --- a/data/config.json +++ b/data/config.json @@ -2,7 +2,6 @@ "evenement": { "nom": "Mon Evenement", "logo": null, - "overlay": null, "couleur_primaire": "#e91e63", "couleur_secondaire": "#ffffff" }, @@ -15,19 +14,43 @@ "impression": true, "email": true, "qr_code": true, - "galerie": true, - "compteur": true + "galerie": true }, "camera": { + "appareil": null, "iso": "auto", "balance_blancs": "auto", - "compte_a_rebours": 3 + "compte_a_rebours": 3, + "animation_compte_a_rebours": "classique" + }, + "compteur": { + "actif": true, + "limite": 400, + "photos_prises": 0 + }, + "destinations": { + "memoire_interne": true, + "cle_usb": false, + "chemin_usb": "/media/usb", + "ftp": false, + "ftp_host": "", + "ftp_port": 21, + "ftp_user": "", + "ftp_password": "", + "ftp_chemin": "/photobooth", + "site_web": false, + "site_web_url": "", + "email_auto": false, + "sauvegarder_tout": true }, "impression": { "imprimante": null, "copies": 1, "format": "10x15" }, + "cadres": { + "actifs": [] + }, "email": { "smtp_host": "", "smtp_port": 587, diff --git a/frontend/css/style.css b/frontend/css/style.css index 81106ce..397d085 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -572,11 +572,291 @@ html, body { border-color: var(--primaire); } +/* === Compteur accueil === */ +.compteur-accueil { + position: absolute; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + background: rgba(0,0,0,0.4); + padding: 0.5rem 1.5rem; + border-radius: 2rem; + font-size: 1.1rem; + color: var(--texte-secondaire); +} + +/* === Admin extras === */ +.btn-petit { + padding: 0.4rem 1rem !important; + font-size: 0.85rem !important; +} + +.champ-row { + display: flex; + gap: 1rem; + align-items: center; +} + +.sous-config { + padding-left: 1.5rem; + border-left: 3px solid var(--primaire); + margin: 0.5rem 0; +} + +h3 { + font-size: 1.1rem; + font-weight: 600; + color: var(--primaire); + margin-top: 0.5rem; +} + +.aide { + color: var(--texte-secondaire); + font-size: 0.85rem; + font-style: italic; +} + +.statut-badge { + display: inline-block; + padding: 0.2rem 0.8rem; + border-radius: 1rem; + font-size: 0.85rem; + background: rgba(76, 175, 80, 0.2); + color: var(--succes); +} + +/* Compteur display */ +.compteur-display { + text-align: center; + padding: 1.5rem; + background: var(--fond); + border-radius: var(--rayon); +} + +.compteur-gros { + display: flex; + align-items: baseline; + justify-content: center; + gap: 0.3rem; +} + +.compteur-nombre { + font-size: 4rem; + font-weight: 700; + color: var(--primaire); +} + +.compteur-nombre.petit { + font-size: 2rem; + color: var(--texte-secondaire); +} + +.compteur-sep { + font-size: 2rem; + color: var(--texte-secondaire); +} + +.compteur-label { + color: var(--texte-secondaire); + font-size: 0.9rem; +} + +/* Radio buttons */ +.radio-groupe { + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.radio { + display: flex; + align-items: center; + gap: 0.8rem; + cursor: pointer; + font-size: 1rem; +} + +.radio input { display: none; } + +.radio-mark { + width: 22px; + height: 22px; + border: 2px solid #555; + border-radius: 50%; + position: relative; + flex-shrink: 0; + transition: var(--transition); +} + +.radio input:checked + .radio-mark { + border-color: var(--primaire); +} + +.radio input:checked + .radio-mark::after { + content: ''; + position: absolute; + top: 4px; left: 4px; + width: 10px; height: 10px; + background: var(--primaire); + border-radius: 50%; +} + +/* Liste cadres */ +.liste-cadres { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.cadre-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.6rem 1rem; + background: var(--fond); + border-radius: 8px; +} + +.cadre-item img { + width: 60px; + height: 40px; + object-fit: contain; + border-radius: 4px; + background: #fff; +} + +.input-fichier { + background: var(--fond); + border: 2px dashed #444; + border-radius: 8px; + padding: 0.8rem; + color: var(--texte); + width: 100%; +} + +/* Liste animations */ +.liste-animations { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.anim-option { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.8rem 1rem; + background: var(--fond); + border: 2px solid transparent; + border-radius: 8px; + cursor: pointer; + transition: var(--transition); +} + +.anim-option.actif { + border-color: var(--primaire); + background: rgba(233, 30, 99, 0.1); +} + +.anim-option:active { + transform: scale(0.98); +} + +.preview-animation-box { + width: 200px; + height: 200px; + background: rgba(0,0,0,0.6); + border-radius: var(--rayon); + display: flex; + align-items: center; + justify-content: center; + margin: 1rem 0; +} + +.anim-chiffre { + font-size: 8rem; + font-weight: 700; + color: var(--primaire); +} + +/* === Animations compte a rebours === */ +/* Classique */ +.anim-classique { animation: anim-pop 0.5s ease; } + +/* Explosion */ +.anim-explosion { animation: anim-explosion 0.6s ease; } +@keyframes anim-explosion { + 0% { transform: scale(0); opacity: 0; } + 50% { transform: scale(1.8); opacity: 1; } + 70% { transform: scale(0.9); } + 100% { transform: scale(1); opacity: 1; } +} + +/* Rebond */ +.anim-rebond { animation: anim-rebond 0.7s ease; } +@keyframes anim-rebond { + 0% { transform: translateY(-200px) scale(0.5); opacity: 0; } + 40% { transform: translateY(20px) scale(1.1); opacity: 1; } + 60% { transform: translateY(-10px) scale(0.95); } + 80% { transform: translateY(5px) scale(1.02); } + 100% { transform: translateY(0) scale(1); } +} + +/* Rotation 3D */ +.anim-rotation { animation: anim-rotation 0.6s ease; perspective: 500px; } +@keyframes anim-rotation { + 0% { transform: rotateY(90deg) scale(0.5); opacity: 0; } + 60% { transform: rotateY(-10deg) scale(1.1); opacity: 1; } + 100% { transform: rotateY(0deg) scale(1); } +} + +/* Fondu */ +.anim-fondu { animation: anim-fondu 0.8s ease; } +@keyframes anim-fondu { + 0% { opacity: 0; transform: scale(2); filter: blur(20px); } + 100% { opacity: 1; transform: scale(1); filter: blur(0); } +} + +/* Emoji fun */ +.anim-emoji { animation: anim-emoji 0.6s ease; } +@keyframes anim-emoji { + 0% { transform: scale(0) rotate(-180deg); } + 60% { transform: scale(1.3) rotate(10deg); } + 100% { transform: scale(1) rotate(0); } +} + +/* Shake */ +.anim-shake { animation: anim-shake 0.6s ease; } +@keyframes anim-shake { + 0% { transform: scale(0.3); opacity: 0; } + 20% { transform: scale(1.1) rotate(-5deg); opacity: 1; } + 40% { transform: scale(1) rotate(5deg); } + 60% { transform: scale(1.05) rotate(-3deg); } + 80% { transform: scale(1) rotate(2deg); } + 100% { transform: scale(1) rotate(0); } +} + +/* Pop (classique redefini) */ +@keyframes anim-pop { + 0% { transform: scale(0.3); opacity: 0; } + 60% { transform: scale(1.2); } + 100% { transform: scale(1); opacity: 1; } +} + +/* Textes emoji pour le mode emoji */ +.emoji-3::before { content: ""; } +.emoji-2::before { content: ""; } +.emoji-1::before { content: ""; } + /* === Utilitaires === */ .cache { display: none !important; } +.texte-secondaire { + color: var(--texte-secondaire); +} + /* Scrollbar tactile */ ::-webkit-scrollbar { width: 6px; diff --git a/frontend/index.html b/frontend/index.html index 49d8710..6836adf 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -17,6 +17,9 @@
Cochez les cadres a proposer aux utilisateurs.
+Aucun cadre importe
+Choisissez le style d'animation pour le 3... 2... 1...
+Aucun cadre importe
'; + return; + } + + for (const nom of data.tous) { + const div = document.createElement('div'); + div.className = 'cadre-item'; + + const cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.checked = data.actifs.includes(nom); + cb.onchange = () => majCadresActifs(); + + const img = document.createElement('img'); + img.src = `/assets/overlays/${nom}`; + img.alt = nom; + + const span = document.createElement('span'); + span.textContent = nom.replace('.png', ''); + + div.appendChild(cb); + div.appendChild(img); + div.appendChild(span); + liste.appendChild(div); + } +} + +async function majCadresActifs() { + const items = document.querySelectorAll('.cadre-item input[type="checkbox"]'); + const noms = document.querySelectorAll('.cadre-item span'); + const actifs = []; + items.forEach((cb, i) => { + if (cb.checked) actifs.push(noms[i].textContent + '.png'); + }); + await apiPost('/api/cadres', { actifs }); +} + +async function uploaderCadre() { + const input = document.getElementById('input-cadre'); + if (!input.files.length) return; + + const formData = new FormData(); + formData.append('fichier', input.files[0]); + + await fetch('/api/upload/overlay', { method: 'POST', body: formData }); + input.value = ''; + chargerCadres(); + afficherStatut('Cadre importe', 'succes'); +} + +// === ANIMATIONS === + +let animationChoisie = 'classique'; + +async function chargerAnimations() { + const anims = await apiGet('/api/animations'); + animationChoisie = config.camera?.animation_compte_a_rebours || 'classique'; + + const liste = document.getElementById('liste-animations'); + liste.innerHTML = ''; + + for (const [id, nom] of Object.entries(anims)) { + const div = document.createElement('div'); + div.className = 'anim-option' + (id === animationChoisie ? ' actif' : ''); + div.textContent = nom; + div.onclick = () => choisirAnimation(id, div); + liste.appendChild(div); + } +} + +async function choisirAnimation(id, el) { + animationChoisie = id; + document.querySelectorAll('.anim-option').forEach(e => e.classList.remove('actif')); + el.classList.add('actif'); + + await apiPost('/api/config', { + camera: { animation_compte_a_rebours: id }, + }); + + testerAnimation(); +} + +const EMOJI_MAP = { 3: '🤪', 2: '😱', 1: '🔥' }; + +function testerAnimation() { + const chiffre = document.getElementById('preview-anim-chiffre'); + let count = 3; + + function afficher() { + if (animationChoisie === 'emoji') { + chiffre.textContent = EMOJI_MAP[count] || count; + } else { + chiffre.textContent = count; + } + chiffre.className = 'anim-chiffre anim-' + animationChoisie; + // Force reflow pour relancer l'animation + chiffre.style.animation = 'none'; + chiffre.offsetHeight; + chiffre.className = 'anim-chiffre anim-' + animationChoisie; + + count--; + if (count > 0) setTimeout(afficher, 800); + } + + afficher(); +} + +// === FONCTIONNALITES === + +function chargerFonctionnalites() { + const fonc = config.fonctionnalites || {}; + for (const [id, cle] of Object.entries(TOGGLES_MAP)) { + const el = document.getElementById(id); + if (el) el.checked = fonc[cle] !== false; + } setupToggleListeners(); } @@ -74,7 +334,6 @@ function setupToggleListeners() { for (const [id, cle] of Object.entries(TOGGLES_MAP)) { const el = document.getElementById(id); if (!el) continue; - // Retirer les anciens listeners en clonant const nouveau = el.cloneNode(true); el.parentNode.replaceChild(nouveau, el); nouveau.addEventListener('change', () => { @@ -83,6 +342,15 @@ function setupToggleListeners() { } } +// === EVENEMENT === + +function chargerEvenementAdmin() { + const event = config.evenement || {}; + setValue('admin-nom-event', event.nom); + setValue('admin-couleur-primaire', event.couleur_primaire || '#e91e63'); + setValue('admin-couleur-secondaire', event.couleur_secondaire || '#ffffff'); +} + async function sauvegarderEvenement() { await apiPost('/api/config', { evenement: { @@ -90,13 +358,21 @@ async function sauvegarderEvenement() { couleur_primaire: getValue('admin-couleur-primaire'), couleur_secondaire: getValue('admin-couleur-secondaire'), }, - camera: { - compte_a_rebours: parseInt(getValue('admin-car')) || 3, - }, }); afficherStatut('Evenement sauvegarde', 'succes'); } +// === EMAIL === + +function chargerEmailAdmin() { + const email = config.email || {}; + setValue('admin-smtp-host', email.smtp_host); + setValue('admin-smtp-port', email.smtp_port || 587); + setValue('admin-smtp-user', email.smtp_user); + setValue('admin-smtp-pass', email.smtp_password); + setValue('admin-expediteur', email.expediteur); +} + async function sauvegarderEmail() { await apiPost('/api/config', { email: { @@ -110,45 +386,43 @@ async function sauvegarderEmail() { afficherStatut('Email sauvegarde', 'succes'); } +// === GALERIE === + +async function chargerGalerieAdmin() { + const compteur = await apiGet('/api/galerie/compteur'); + document.getElementById('admin-nb-photos').textContent = compteur.photos_prises || 0; + document.getElementById('admin-nb-exports').textContent = compteur.exports || 0; + setValue('admin-url-galerie', (config.qr_code || {}).url_galerie); +} + async function sauvegarderGalerie() { await apiPost('/api/config', { qr_code: { url_galerie: getValue('admin-url-galerie') }, - impression: { - imprimante: getValue('admin-imprimante'), - copies: parseInt(getValue('admin-copies')) || 1, - }, }); afficherStatut('Configuration sauvegardee', 'succes'); } -async function reconnecterCamera() { - const resultat = await apiPost('/api/camera/reconnecter'); - document.getElementById('admin-camera-statut').textContent = - resultat.connectee ? 'Connectee' : 'Deconnectee'; -} - function confirmerViderGalerie() { if (confirm('Supprimer TOUTES les photos ? Cette action est irreversible.')) { apiPost('/api/galerie/vider').then(() => { afficherStatut('Galerie videe', 'succes'); - chargerAdmin(); + chargerGalerieAdmin(); }); } } +// === SYSTEME === + function confirmerRedemarrage() { - if (confirm('Redemarrer le systeme ?')) { - apiPost('/api/systeme/redemarrer'); - } + if (confirm('Redemarrer le systeme ?')) apiPost('/api/systeme/redemarrer'); } function confirmerExtinction() { - if (confirm('Eteindre le systeme ?')) { - apiPost('/api/systeme/eteindre'); - } + if (confirm('Eteindre le systeme ?')) apiPost('/api/systeme/eteindre'); } -// Utilitaires +// === Utilitaires === + function setValue(id, val) { const el = document.getElementById(id); if (el) el.value = val || ''; diff --git a/frontend/js/app.js b/frontend/js/app.js index 52a94be..86f9984 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -52,6 +52,7 @@ function allerA(ecran) { photosSession = []; photoFinale = null; arreterPreview(); + majCompteurAccueil(); } else if (ecran === 'capture') { lancerCapture(); } else if (ecran === 'galerie') { diff --git a/frontend/js/camera.js b/frontend/js/camera.js index e0cda62..79f4793 100644 --- a/frontend/js/camera.js +++ b/frontend/js/camera.js @@ -3,16 +3,17 @@ let previewActif = false; let previewInterval = null; +const EMOJI_CAR = { 3: '🤪', 2: '😱', 1: '🔥' }; + // --- Preview live --- function lancerPreview() { if (previewActif) return; previewActif = true; - // Demander des previews via WebSocket previewInterval = setInterval(() => { if (previewActif) wsEnvoyer({ type: 'preview' }); - }, 100); // ~10 fps + }, 100); } function arreterPreview() { @@ -23,7 +24,6 @@ function arreterPreview() { } } -// Recevoir les previews wsOnMessage('preview', (msg) => { if (!previewActif) return; const img = document.getElementById('img-preview'); @@ -35,6 +35,7 @@ wsOnMessage('preview', (msg) => { async function lancerCapture() { photosSession = []; lancerPreview(); + majCompteurAccueil(); const nbPhotos = getNombrePhotos(); const compteurEl = document.getElementById('capture-compteur'); @@ -44,27 +45,28 @@ async function lancerCapture() { compteurEl.textContent = `Photo ${i + 1} / ${nbPhotos}`; } - // Compte a rebours await compteARebours(); - - // Flash afficherFlash(); - // Capture arreterPreview(); const resultat = await apiPost('/api/capturer'); + + if (resultat.erreur) { + // Limite atteinte ou erreur + allerA('accueil'); + return; + } + if (resultat.nom) { photosSession.push(resultat.nom); } - // Reprendre le preview si encore des photos a prendre if (i < nbPhotos - 1) { lancerPreview(); await pause(500); } } - // Traitement selon le mode await traiterCapture(); } @@ -78,15 +80,24 @@ async function compteARebours() { const conteneur = document.getElementById('compte-a-rebours'); const chiffre = document.getElementById('chiffre-car'); const duree = config.camera?.compte_a_rebours || 3; + const anim = config.camera?.animation_compte_a_rebours || 'classique'; conteneur.classList.remove('cache'); for (let i = duree; i > 0; i--) { - chiffre.textContent = i; - // Re-trigger animation + // Contenu selon le type d'animation + if (anim === 'emoji') { + chiffre.textContent = EMOJI_CAR[i] || i; + } else { + chiffre.textContent = i; + } + + // Appliquer l'animation + chiffre.className = 'anim-chiffre anim-' + anim; chiffre.style.animation = 'none'; - chiffre.offsetHeight; // force reflow - chiffre.style.animation = 'pop 0.5s ease'; + chiffre.offsetHeight; + chiffre.className = 'anim-chiffre anim-' + anim; + await pause(1000); } @@ -109,7 +120,6 @@ async function traiterCapture() { } if (modeActuel === 'multi') { - // Creer le strip/collage const mode = config.multi_shot?.mode || 'strip'; let resultat; if (mode === 'strip') { @@ -122,7 +132,6 @@ async function traiterCapture() { afficherPreviewPhoto('/data/exports/' + resultat.nom); } } else { - // Photo simple - aller au preview avec filtres photoFinale = photosSession[0]; afficherPreviewPhoto('/data/photos/' + photosSession[0]); } @@ -134,17 +143,28 @@ function afficherPreviewPhoto(chemin) { document.getElementById('photo-resultat').src = chemin; document.getElementById('photo-partage').src = chemin; - // Charger les filtres si photo simple if (modeActuel === 'simple' && config.fonctionnalites?.filtres) { chargerFiltres(); } - - // Charger les overlays if (config.fonctionnalites?.overlays) { chargerOverlays(); } } +// --- Compteur accueil --- + +async function majCompteurAccueil() { + const etat = await apiGet('/api/compteur'); + const el = document.getElementById('compteur-accueil'); + if (etat.actif) { + el.classList.remove('cache'); + document.getElementById('compteur-restant').textContent = etat.restantes; + document.getElementById('compteur-limite').textContent = etat.limite; + } else { + el.classList.add('cache'); + } +} + function pause(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }