From d9de999d7438c6e9f7c591eb71cb86b9105e5996 Mon Sep 17 00:00:00 2001 From: Jules Date: Sat, 21 Mar 2026 23:03:30 +0100 Subject: [PATCH] Backoffice complet : materiel, compteur, destinations, cadres, animations - Onglet Materiel : choix appareil photo + imprimante - Compteur avec limite configurable (399/400) affiche sur l'accueil - Destinations multiples : memoire interne, cle USB, FTP, site web, email - Choix sauvegarder toutes les photos ou seulement les imprimees - Gestion des cadres : activation/desactivation + import - 7 animations de compte a rebours : classique, explosion, rebond, rotation 3D, fondu, emoji fun, tremblement - Preview des animations dans le backoffice - Module destinations.py (copie USB, envoi FTP, compteur) Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/destinations.py | 114 ++++++++++++ backend/main.py | 67 ++++++- data/config.json | 31 +++- frontend/css/style.css | 280 +++++++++++++++++++++++++++++ frontend/index.html | 220 ++++++++++++++++------- frontend/js/admin.js | 388 ++++++++++++++++++++++++++++++++++------ frontend/js/app.js | 1 + frontend/js/camera.js | 56 ++++-- 8 files changed, 1009 insertions(+), 148 deletions(-) create mode 100644 backend/destinations.py 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 @@
+
+ 400 / 400 +
@@ -58,10 +61,8 @@ Photo
-
-
@@ -89,17 +90,14 @@ QR Code
- -
QR Code
-
@@ -110,43 +108,154 @@

Galerie

-
- -
+
- +
-

Administration

+

Parametres

-
- + + + + + - - +
- -
+ +
+

Appareil photo

+
+ + -- + +
+
+ + +
+
+ + +
+ +

Imprimante

+
+ + + +
+
+ + +
+ +
+ + +
+

Compteur de photos

+
+ +
+
+
+ 400 + / + 400 +
+ photos restantes +
+
+ + +
+
+ + +
+
+ + +
+

Ou sauvegarder les photos ?

- - - - - - - - - - + + +
+
+
+ +
+ + +
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+ +

Quelles photos sauvegarder ?

+
+ + +
+ +
+ + +
+

Cadres disponibles

+

Cochez les cadres a proposer aux utilisateurs.

+
+

Aucun cadre importe

+
+

Importer un cadre

+
+ + +
+
+ + +
+

Animation du compte a rebours

+

Choisissez le style d'animation pour le 3... 2... 1...

+
+
+
+

Apercu

+
+ 3 +
+
@@ -167,53 +276,28 @@
- -
-
- - -- - -
-
- - -
-
- - -
-
- - -
-
- - + +
+
+ + + + + + + + +
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
+
+
+
+
+
diff --git a/frontend/js/admin.js b/frontend/js/admin.js index e05768e..7b79580 100644 --- a/frontend/js/admin.js +++ b/frontend/js/admin.js @@ -1,6 +1,6 @@ -/* Module administration - Menu cache */ +/* Module administration - Backoffice complet */ -// Mapping toggle ID -> cle config +// Mapping toggle ID -> cle config fonctionnalites const TOGGLES_MAP = { 'tog-photo-simple': 'photo_simple', 'tog-multi-shot': 'multi_shot', @@ -11,36 +11,53 @@ const TOGGLES_MAP = { 'tog-email': 'email', 'tog-qr-code': 'qr_code', 'tog-galerie': 'galerie', - 'tog-compteur': 'compteur', }; async function chargerAdmin() { config = await apiGet('/api/config'); - const fonc = config.fonctionnalites || {}; - const event = config.evenement || {}; + chargerMateriel(); + chargerCompteur(); + chargerDestinations(); + chargerCadres(); + chargerAnimations(); + chargerFonctionnalites(); + chargerEmailAdmin(); + chargerGalerieAdmin(); +} + +// === MATERIEL === + +async function chargerMateriel() { const cam = config.camera || {}; const imp = config.impression || {}; - const email = config.email || {}; - const qr = config.qr_code || {}; - - // Toggles fonctionnalites - for (const [id, cle] of Object.entries(TOGGLES_MAP)) { - const el = document.getElementById(id); - if (el) el.checked = fonc[cle] !== false; - } - - // Evenement - setValue('admin-nom-event', event.nom); - setValue('admin-couleur-primaire', event.couleur_primaire || '#e91e63'); - setValue('admin-couleur-secondaire', event.couleur_secondaire || '#ffffff'); // Camera const statut = await apiGet('/api/camera/statut'); - document.getElementById('admin-camera-statut').textContent = - statut.connectee ? 'Connectee' : 'Deconnectee'; + const badge = document.getElementById('admin-camera-statut'); + badge.textContent = statut.connectee ? 'Connectee' : 'Deconnectee'; + badge.style.background = statut.connectee ? 'rgba(76,175,80,0.2)' : 'rgba(244,67,54,0.2)'; + badge.style.color = statut.connectee ? 'var(--succes)' : 'var(--danger)'; + + // Liste appareils + const selectApp = document.getElementById('admin-appareil'); + selectApp.innerHTML = ''; + for (const a of statut.appareils) { + const opt = document.createElement('option'); + opt.value = a; + opt.textContent = a; + selectApp.appendChild(opt); + } + if (cam.appareil) selectApp.value = cam.appareil; + setValue('admin-car', cam.compte_a_rebours || 3); - // Impression + // Imprimantes + await rafraichirImprimantes(); + if (imp.imprimante) document.getElementById('admin-imprimante').value = imp.imprimante; + setValue('admin-copies', imp.copies || 1); +} + +async function rafraichirImprimantes() { const imprimantes = await apiGet('/api/imprimantes'); const select = document.getElementById('admin-imprimante'); select.innerHTML = ''; @@ -50,23 +67,266 @@ async function chargerAdmin() { opt.textContent = `${p.nom} (${p.statut})`; select.appendChild(opt); } - if (imp.imprimante) select.value = imp.imprimante; - setValue('admin-copies', imp.copies || 1); +} - // 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 reconnecterCamera() { + const resultat = await apiPost('/api/camera/reconnecter'); + chargerMateriel(); +} - // Galerie - 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', qr.url_galerie); +async function sauvegarderMateriel() { + await apiPost('/api/config', { + camera: { + appareil: getValue('admin-appareil'), + compte_a_rebours: parseInt(getValue('admin-car')) || 3, + }, + impression: { + imprimante: getValue('admin-imprimante'), + copies: parseInt(getValue('admin-copies')) || 1, + }, + }); + afficherStatut('Materiel sauvegarde', 'succes'); +} - // Ecouter les changements de toggles +// === COMPTEUR === + +async function chargerCompteur() { + const etat = await apiGet('/api/compteur'); + document.getElementById('tog-compteur-actif').checked = etat.actif; + document.getElementById('admin-compteur-restant').textContent = etat.restantes; + document.getElementById('admin-compteur-limite-display').textContent = etat.limite; + setValue('admin-compteur-limite', etat.limite); +} + +async function sauvegarderCompteur() { + await apiPost('/api/config', { + compteur: { + actif: document.getElementById('tog-compteur-actif').checked, + limite: parseInt(getValue('admin-compteur-limite')) || 400, + }, + }); + chargerCompteur(); + afficherStatut('Compteur sauvegarde', 'succes'); +} + +async function resetCompteur() { + if (confirm('Remettre le compteur a zero ?')) { + await apiPost('/api/compteur/reset'); + chargerCompteur(); + afficherStatut('Compteur remis a zero', 'succes'); + } +} + +// === DESTINATIONS === + +function chargerDestinations() { + const dest = config.destinations || {}; + + document.getElementById('tog-dest-memoire').checked = dest.memoire_interne !== false; + document.getElementById('tog-dest-usb').checked = dest.cle_usb || false; + document.getElementById('tog-dest-ftp').checked = dest.ftp || false; + document.getElementById('tog-dest-web').checked = dest.site_web || false; + document.getElementById('tog-dest-email-auto').checked = dest.email_auto || false; + + // Sous-configs visibles si actives + toggleSousConfig('tog-dest-usb', 'dest-usb-details'); + toggleSousConfig('tog-dest-ftp', 'dest-ftp-details'); + toggleSousConfig('tog-dest-web', 'dest-web-details'); + + setValue('admin-ftp-host', dest.ftp_host); + setValue('admin-ftp-port', dest.ftp_port || 21); + setValue('admin-ftp-user', dest.ftp_user); + setValue('admin-ftp-pass', dest.ftp_password); + setValue('admin-ftp-chemin', dest.ftp_chemin || '/photobooth'); + setValue('admin-web-url', dest.site_web_url); + + // Radio sauvegarder tout / imprimees + if (dest.sauvegarder_tout === false) { + document.getElementById('sauv-imprimees').checked = true; + } else { + document.getElementById('sauv-tout').checked = true; + } + + // Listeners pour afficher/cacher sous-configs + ['tog-dest-usb', 'tog-dest-ftp', 'tog-dest-web'].forEach(id => { + const el = document.getElementById(id); + el.onchange = () => { + const map = { 'tog-dest-usb': 'dest-usb-details', 'tog-dest-ftp': 'dest-ftp-details', 'tog-dest-web': 'dest-web-details' }; + toggleSousConfig(id, map[id]); + }; + }); +} + +function toggleSousConfig(toggleId, detailsId) { + const checked = document.getElementById(toggleId).checked; + const el = document.getElementById(detailsId); + if (checked) el.classList.remove('cache'); + else el.classList.add('cache'); +} + +async function detecterUSB() { + const usbs = await apiGet('/api/usb/detecter'); + const select = document.getElementById('admin-chemin-usb'); + select.innerHTML = ''; + if (usbs.length === 0) { + const opt = document.createElement('option'); + opt.value = ''; + opt.textContent = 'Aucune cle detectee'; + select.appendChild(opt); + } else { + for (const u of usbs) { + const opt = document.createElement('option'); + opt.value = u; + opt.textContent = u; + select.appendChild(opt); + } + } +} + +async function sauvegarderDestinations() { + await apiPost('/api/config', { + destinations: { + memoire_interne: document.getElementById('tog-dest-memoire').checked, + cle_usb: document.getElementById('tog-dest-usb').checked, + chemin_usb: getValue('admin-chemin-usb') || '/media/usb', + ftp: document.getElementById('tog-dest-ftp').checked, + ftp_host: getValue('admin-ftp-host'), + ftp_port: parseInt(getValue('admin-ftp-port')) || 21, + ftp_user: getValue('admin-ftp-user'), + ftp_password: getValue('admin-ftp-pass'), + ftp_chemin: getValue('admin-ftp-chemin') || '/photobooth', + site_web: document.getElementById('tog-dest-web').checked, + site_web_url: getValue('admin-web-url'), + email_auto: document.getElementById('tog-dest-email-auto').checked, + sauvegarder_tout: document.getElementById('sauv-tout').checked, + }, + }); + afficherStatut('Destinations sauvegardees', 'succes'); +} + +// === CADRES === + +async function chargerCadres() { + const data = await apiGet('/api/cadres'); + const liste = document.getElementById('liste-cadres'); + liste.innerHTML = ''; + + if (data.tous.length === 0) { + liste.innerHTML = '

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