diff --git a/backend/main.py b/backend/main.py index b5a7b88..fafba97 100644 --- a/backend/main.py +++ b/backend/main.py @@ -10,7 +10,7 @@ from contextlib import asynccontextmanager import cv2 from fastapi import FastAPI, WebSocket, WebSocketDisconnect, UploadFile, File -from fastapi.responses import FileResponse, JSONResponse +from fastapi.responses import FileResponse, JSONResponse, HTMLResponse from fastapi.staticfiles import StaticFiles from backend.config import ( @@ -155,6 +155,63 @@ async def page_principale(): return FileResponse(str(RACINE / "frontend" / "index.html")) +@app.post("/api/sessions/{code}/upload") +async def api_session_upload(code: str, fichier: UploadFile = File(...)): + """Upload d'une photo dans une session identifiรฉe par code ร  9 chiffres.""" + if not code.isdigit() or len(code) != 9: + return JSONResponse({"erreur": "Code invalide"}, status_code=400) + dossier = DOSSIER_PHOTOS / f"session_{code}" + dossier.mkdir(parents=True, exist_ok=True) + dest = dossier / fichier.filename + dest.write_bytes(await fichier.read()) + return {"ok": True, "fichier": fichier.filename} + + +@app.get("/s/{code}", response_class=HTMLResponse) +async def galerie_session(code: str): + """Galerie mobile pour une session photo (QR code).""" + if not code.isdigit() or len(code) != 9: + return HTMLResponse("

Code invalide

", status_code=404) + dossier = DOSSIER_PHOTOS / f"session_{code}" + cfg = charger_config() + nom_event = cfg.get("evenement", {}).get("nom", "Photobooth") + photos = sorted(dossier.glob("*.jpg"), key=lambda p: p.stat().st_mtime, reverse=True) if dossier.exists() else [] + items = "" + for p in photos: + items += f'
\n' + if not items: + items = '

Les photos apparaรฎtront ici aprรจs chaque prise ๐Ÿ“ท

' + html = f""" + + +{nom_event} โ€” Galerie + +

๐Ÿ“ท {nom_event}

Code session : {code}

+
{items}
+ + +""" + return HTMLResponse(html) + + # --- API Config --- @app.get("/api/config") diff --git a/photobooth_pygame.py b/photobooth_pygame.py new file mode 100644 index 0000000..0e43043 --- /dev/null +++ b/photobooth_pygame.py @@ -0,0 +1,1914 @@ +#!/usr/bin/env python3 +""" +Photobooth Pygame - SikaPics / CopyDev / Base +Wizard + thรจmes + design qualitรฉ +""" +import sys, time, threading, io, math, json, os, smtplib, subprocess, traceback, faulthandler +from pathlib import Path +from datetime import datetime +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.base import MIMEBase +from email import encoders + +import pygame +import gphoto2 as gp +from PIL import Image, ImageDraw, ImageFont, ImageOps + +try: import qrcode as qrlib; HAS_QR = True +except: HAS_QR = False +try: import cups; HAS_CUPS = True +except: HAS_CUPS = False +try: + from flask import Flask as _Flask, request as _req, jsonify as _jsonify, redirect as _redir + HAS_FLASK = True +except ImportError: + HAS_FLASK = False + +faulthandler.enable() + +# โ”€โ”€ Chemins โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +BASE = Path(__file__).parent +CONFIG_PATH = BASE / "data/config.json" +LOGOS_DIR = BASE / "frontend/assets/logos" +PHOTOS_DIR = BASE / "data/photos" +PHOTOS_DIR.mkdir(parents=True, exist_ok=True) + +W, H = 1920, 1280 + +# โ”€โ”€ Thรจmes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +THEMES = { + "base": { + "nom": "Sombre", + "fond": (26, 26, 46), + "fond2": (16, 21, 62), + "carte": (22, 33, 62), + "prim": (233, 30, 99), + "texte": (255, 255, 255), + "texte2": (160, 160, 176), + "clair": False, + "logo": None, + }, + "sikapics": { + "nom": "SikaPics", + "fond": (251, 231, 232), + "fond2": (255, 240, 241), + "carte": (255, 255, 255), + "prim": (195, 31, 72), + "texte": (36, 4, 57), + "texte2": (120, 90, 110), + "clair": True, + "logo": "sikapics.png", + }, + "copydev": { + "nom": "CopyDev", + "fond": (8, 18, 40), + "fond2": (12, 28, 60), + "carte": (18, 42, 84), + "prim": (41, 182, 246), + "texte": (255, 255, 255), + "texte2": (140, 185, 230), + "clair": False, + "logo": "copydev.png", + }, + "photobooth": { + "nom": "Photobooth", + "fond": (235, 242, 255), + "fond2": (220, 232, 255), + "carte": (255, 255, 255), + "prim": (25, 100, 230), + "texte": (15, 30, 80), + "texte2": (90, 120, 180), + "clair": True, + "logo": None, + }, +} + +# โ”€โ”€ ร‰tats โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +S_ACCUEIL = "accueil" +S_MODE = "mode" +S_PREV = "preview" +S_COMPTE = "compte" +S_CAP = "capture" +S_GAL = "galerie" +S_SHARE = "partage" +S_EMAIL = "email" +S_WIZ = "wizard" +S_ADMIN = "admin" +S_FORMAT = "format" +S_PIN = "pin" + +ADMIN_TABS = [ + ("fonctions", "Fonctions"), + ("materiel", "Matรฉriel"), + ("compteur", "Compteur"), + ("evenement", "ร‰vรฉnement"), + ("systeme", "Systรจme"), +] + +# Formats multipose : (key, label, nb_photos, description) +FORMATS = [ + ("grille_4", "Grille 2ร—2", 4, "4 poses ยท collage carrรฉ"), + ("bande3x2", "Bande ร— 2", 3, "3 poses ยท bande doublรฉe"), + ("bande4x2", "Bande ร— 2", 4, "4 poses ยท bande doublรฉe"), +] + + +# โ”€โ”€ Web admin mobile (Flask sur port 5000) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +_web_state = {"reload_flag": threading.Event(), "ip": None} + +_ADMIN_HTML = """ + +Admin โ€” Photobooth + + +
+

โš™ Administration

+ Chargement... +
+
+
+
ร‰vรฉnement en cours
+
...
+
+
+
+
Fonctionnalitรฉs
+
Photo simple
+
Pellicule
+
Impression
+
Email
+
QR Code
+
+
+
Compteur photos
+
...
+ +
+Configurer un รฉvรฉnement (Wizard) +
+
+ +""" + +_WIZARD_HTML = """ + +Wizard โ€” Photobooth + + +
+

Configuration รฉvรฉnement

+
+
+
+ +
+

1 โ€” Nom de l'รฉvรฉnement

+
+ +
+ +
+ +
+

2 โ€” Thรจme visuel

+
+
Sombre
+
SikaPics
+
CopyDev
+
+ +
+ +
+

3 โ€” Modes photo

+
+
Photo simple (1 prise)
+
Pellicule (plusieurs poses)
+
+ +
+ +
+

4 โ€” Photos par pellicule

+
+
+ +
4
+ +
+
+ +
+ +
+

5 โ€” Options de partage

+
+
Impression
+
Email
+
QR Code galerie
+
Galerie
+
+ +
+ +
+

6 โ€” Rรฉsumรฉ

+
+ +
+
+ + +""" + + +def _start_web_server(): + if not HAS_FLASK: + print("Flask absent โ€” admin web dรฉsactivรฉ (pip install flask)") + return + import logging, socket + logging.getLogger('werkzeug').setLevel(logging.ERROR) + fa = _Flask(__name__) + + @fa.route("/") + def _idx(): return _redir("/admin") + + @fa.route("/admin") + def _adm(): return _ADMIN_HTML + + @fa.route("/wizard") + def _wiz(): return _WIZARD_HTML + + @fa.route("/api/config") + def _gcfg(): + try: return _jsonify(json.loads(CONFIG_PATH.read_text())) + except: return _jsonify({}) + + @fa.route("/api/config", methods=["POST"]) + def _scfg(): + try: + CONFIG_PATH.write_text(json.dumps(_req.get_json(), indent=2, ensure_ascii=False)) + _web_state["reload_flag"].set() + return _jsonify({"ok": True}) + except Exception as e: + return _jsonify({"ok": False, "err": str(e)}) + + @fa.route("/api/wizard", methods=["POST"]) + def _awiz(): + try: + w = _req.get_json() + c = json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {} + c.setdefault("evenement", {}).update({"nom": w.get("nom",""), "theme": w.get("theme","base")}) + c.setdefault("multi_shot", {})["nombre_photos"] = int(w.get("nb_photos", 4)) + f = c.setdefault("fonctionnalites", {}) + f.update(w.get("modes", {})) + f.update(w.get("share", {})) + CONFIG_PATH.write_text(json.dumps(c, indent=2, ensure_ascii=False)) + _web_state["reload_flag"].set() + return _jsonify({"ok": True}) + except Exception as e: + return _jsonify({"ok": False, "err": str(e)}) + + @fa.route("/api/status") + def _status(): + try: ip = subprocess.check_output("hostname -I", shell=True).decode().split()[0] + except: ip = "?" + return _jsonify({"ip": ip}) + + try: ip = subprocess.check_output("hostname -I", shell=True).decode().split()[0].strip() + except: ip = "?" + for port in (80, 8000, 8001): + url = f"http://{ip}" + (f":{port}" if port != 80 else "") + try: + _web_state["ip"] = ip if port == 80 else f"{ip}:{port}" + print(f"Web admin mobile: {url}/ (wizard: {url}/wizard)") + fa.run(host="0.0.0.0", port=port, threaded=True, use_reloader=False) + break + except OSError: + print(f"Port {port} indisponible, essai suivant...") + + +def cfg_load(): + try: return json.loads(CONFIG_PATH.read_text()) + except: return {} + +def cfg_save(c): + try: CONFIG_PATH.write_text(json.dumps(c, indent=2, ensure_ascii=False)) + except: pass + + +class PB: + # โ”€โ”€ Init โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def __init__(self): + pygame.init() + pygame.mouse.set_visible(False) + self.scr = pygame.display.set_mode((W, H), pygame.FULLSCREEN) + self.clock = pygame.time.Clock() + + fn = "dejavusans" + self.F = { + "xxl": pygame.font.SysFont(fn, 300, bold=True), + "xl": pygame.font.SysFont(fn, 150, bold=True), + "lg": pygame.font.SysFont(fn, 96, bold=True), + "md": pygame.font.SysFont(fn, 68), + "sm": pygame.font.SysFont(fn, 50), + "xs": pygame.font.SysFont(fn, 36), + "xxs": pygame.font.SysFont(fn, 26), + } + + self.cfg = cfg_load() + self.T = THEMES.get(self.cfg.get("evenement", {}).get("theme", "base"), THEMES["base"]) + self._bg_cache = {} + self._logo_cache = {} + + # โ”€โ”€ Camรฉra โ”€โ”€ + self.sim = "--sim" in sys.argv + self.cam = None + if not self.sim: + for i in range(3): + try: + self.cam = gp.Camera(); self.cam.init() + print(f"Camera OK ({i+1})"); break + except Exception as e: + print(f" cam {i+1}/3: {e}"); self.cam = None; time.sleep(1) + if not self.cam: + print("Camera absente โ†’ simulation"); self.sim = True + else: + print("Mode simulation") + + # โ”€โ”€ ร‰tat โ”€โ”€ + self.etat = S_ACCUEIL + self.mode = "simple" + self.format = self.cfg.get("multi_shot",{}).get("format","grille_4") + self.frame = None + self.flock = threading.Lock() + self.prev_actif = not self.sim # miroir levรฉ dรจs le dรฉmarrage + self.cap_demande = False + self.cap_result = None + self.cap_done = threading.Event() + self.photos = [] + self.num_ph = 0 + self.chiffre = 0 + self.photo_active = None + self.gal_surfs = [] + self.gal_ready = False + self.session_qr = None + self.session_url = None + self.t0 = time.time() + self.admin_t = 0 + self.admin_tab = "fonctions" + self.pin_buf = "" + self.pin_err = False + self._sys_info = {} + self._sys_info_t = 0 + self.email_buf = "" + self.share_msg = "" + self.email_msg = "" + self.compteur = self.cfg.get("compteur", {}).get("photos_prises", 0) + self.cpt_max = self.cfg.get("compteur", {}).get("limite", 400) + + # โ”€โ”€ Wizard โ”€โ”€ + self.wiz_step = 1 + self.wiz_data = {} + self.wiz_kbbuf = "" # clavier wizard + self.wiz_code_buf = "" # code session step 2 + self.wiz_code_status = None # None / "test..." / "ok" / "erreur" + + # โ”€โ”€ IP locale (pour QR codes) โ”€โ”€ + try: self._local_ip = subprocess.check_output("hostname -I", shell=True).decode().split()[0].strip() + except: self._local_ip = "?" + + # โ”€โ”€ Surveillance config (sync FastAPI + web mobile) โ”€โ”€ + self._cfg_mtime = CONFIG_PATH.stat().st_mtime if CONFIG_PATH.exists() else 0 + self._cfg_check = time.time() + self._admin_qr = None # QR code URL admin mobile (gรฉnรฉrรฉ lazy) + + # โ”€โ”€ QR galerie (vers le web server comme la premiรจre version) โ”€โ”€ + self.qr_surf = self._make_qr() + + threading.Thread(target=self._cam_loop, daemon=True).start() + threading.Thread(target=_start_web_server, daemon=True).start() + + # โ”€โ”€ Thรจme โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _apply_theme(self, key): + self.T = THEMES.get(key, THEMES["base"]) + self._bg_cache = {} + + def _bg(self): + k = id(self.T) + if k not in self._bg_cache: + c1, c2 = self.T["fond"], self.T["fond2"] + s = pygame.Surface((1, H)) + for y in range(H): + t = y/(H-1) + s.set_at((0,y), tuple(int(c1[i]*(1-t)+c2[i]*t) for i in range(3))) + self._bg_cache[k] = pygame.transform.scale(s, (W, H)) + self.scr.blit(self._bg_cache[k], (0,0)) + + def _logo(self): + k = self.T["logo"] + if k is None: return None + if k not in self._logo_cache: + p = LOGOS_DIR / k + if p.exists(): + raw = pygame.image.load(str(p)).convert_alpha() + lw = int(W * 0.38) + lh = int(raw.get_height() * lw / raw.get_width()) + self._logo_cache[k] = pygame.transform.smoothscale(raw, (lw, lh)) + else: + self._logo_cache[k] = None + return self._logo_cache.get(k) + + # โ”€โ”€ Helpers dessin โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _card(self, rect, bord=None, fond=None, rayon=22, bord_w=2): + bord = bord or self.T["prim"] + fond = fond or self.T["carte"] + # Ombre + ss = pygame.Surface((rect.w+8, rect.h+8), pygame.SRCALPHA) + pygame.draw.rect(ss, (0,0,0,60), (4,4,rect.w,rect.h), border_radius=rayon) + self.scr.blit(ss, (rect.x-2, rect.y-2)) + # Fond + pygame.draw.rect(self.scr, fond, rect, border_radius=rayon) + # Lueur bord + for i in range(5,0,-1): + gs = pygame.Surface((rect.w+i*2, rect.h+i*2), pygame.SRCALPHA) + a = int(30*i/5) + pygame.draw.rect(gs, (*bord,a), (0,0,rect.w+i*2,rect.h+i*2), + border_radius=rayon+i, width=2) + self.scr.blit(gs, (rect.x-i, rect.y-i)) + # Bordure + pygame.draw.rect(self.scr, bord, rect, bord_w, border_radius=rayon) + + def _btn(self, rect, couleur, texte, fkey="sm", rayon=16, tc=None): + tc = tc or (self.T["texte"] if self.T["clair"] and couleur==self.T["carte"] else (255,255,255)) + # Ombre + pygame.draw.rect(self.scr, (0,0,0), pygame.Rect(rect.x+3,rect.y+5,rect.w,rect.h), border_radius=rayon) + # Corps + pygame.draw.rect(self.scr, couleur, rect, border_radius=rayon) + # Reflet haut + rs = pygame.Surface((rect.w, rect.h//2), pygame.SRCALPHA) + rs.fill((255,255,255,18)) + self.scr.blit(rs, (rect.x, rect.y)) + # Texte + t = self.F[fkey].render(texte, True, tc) + self.scr.blit(t, (rect.centerx-t.get_width()//2, rect.centery-t.get_height()//2)) + return rect + + def _txt(self, texte, fkey, c=None, shadow=True): + c = c or self.T["texte"] + f = self.F[fkey] + s = f.render(texte, True, c) + if not shadow: return s + so = f.render(texte, True, (0,0,0) if not self.T["clair"] else (255,255,255)) + so.set_alpha(60) + surf = pygame.Surface((s.get_width()+2, s.get_height()+2), pygame.SRCALPHA) + surf.blit(so, (2,2)); surf.blit(s, (0,0)) + return surf + + def _c(self, surf, y): + self.scr.blit(surf, (W//2-surf.get_width()//2, y)) + + def _overlay(self, a=130): + s = pygame.Surface((W,H), pygame.SRCALPHA); s.fill((0,0,0,a)); self.scr.blit(s,(0,0)) + + def _lv(self): + if self.sim: + t = time.time() + v = int(30+15*math.sin(t*2)) + self.scr.fill((0,v,0)) + s = self.F["xs"].render("[SIMULATION - PAS DE DSLR]", True, (255,255,255)) + self.scr.blit(s,(W//2-s.get_width()//2,H//2-s.get_height()//2)) + return + with self.flock: + if self.frame: self.scr.blit(self.frame,(0,0)) + else: self.scr.fill(self.T["fond"]) + + def _progress_bar(self, step, total, y=40): + bw = 600; bx = W//2-bw//2; bh = 8 + pygame.draw.rect(self.scr, self.T["carte"], (bx,y,bw,bh), border_radius=4) + fw = int(bw * step/total) + if fw > 0: + pygame.draw.rect(self.scr, self.T["prim"], (bx,y,fw,bh), border_radius=4) + + def _make_qr(self): + if not HAS_QR: return None + # 1. Code session booth (gรฉnรฉrรฉ par le wizard) โ†’ prioritรฉ absolue + b = self.cfg.get("booth", {}) + code = b.get("session_code", "") + if code: + url = f"{b.get('url','https://photobooth.copydev.fr').rstrip('/')}/s/{code}" + else: + # 2. URL galerie configurรฉe manuellement + url = self.cfg.get("qr_code",{}).get("url_galerie","") + if not url: + return None + try: + img = qrlib.make(url, border=2).convert("RGB").resize((200,200),Image.NEAREST) + return pygame.image.fromstring(img.tobytes(),img.size,"RGB") + except: return None + + # โ”€โ”€ Thread camรฉra โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _cam_loop(self): + if self.sim: return + errs = 0 + while True: + if self.prev_actif: + try: + if self.cam is None: + raise RuntimeError("camera None") + if self.cap_demande: + # Capture tethered LV : trigger_capture + wait_for_event + # โ†’ le miroir reste levรฉ (dรฉjร  en liveview) + self.cam.trigger_capture() + ch = None + deadline = time.time() + 15 + while time.time() < deadline: + ev_type, ev_data = self.cam.wait_for_event(500) + if ev_type == gp.GP_EVENT_FILE_ADDED: + cf = gp.CameraFile() + self.cam.file_get(ev_data.folder, ev_data.name, gp.GP_FILE_TYPE_NORMAL, cf) + nom = f"photo_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.jpg" + ch = str(PHOTOS_DIR/nom); cf.save(ch); break + self.cap_result=ch; self.cap_demande=False; self.cap_done.set() + errs = 0 + else: + f=self.cam.capture_preview(); data=bytes(f.get_data_and_size()) + surf=pygame.image.load(io.BytesIO(data)) + surf=pygame.transform.scale(surf,(W,H)) + with self.flock: self.frame=surf + errs = 0 + except Exception as e: + errs += 1 + print(f"cam erreur ({errs}): {e}") + if self.cap_demande and errs >= 3: + # abandon de la capture en cours + self.cap_result=None; self.cap_demande=False; self.cap_done.set() + if errs >= 5: + # reconnexion automatique + print("Camรฉra perdue โ€” tentative de reconnexion...") + self.prev_actif = False + with self.flock: self.frame = None + try: + if self.cam: self.cam.exit() + except: pass + self.cam = None; errs = 0 + while True: + time.sleep(3) + try: + c = gp.Camera(); c.init() + self.cam = c; self.prev_actif = True + print("Camรฉra reconnectรฉe automatiquement") + break + except Exception as re: + print(f"Reconnexion รฉchouรฉe: {re}") + else: + time.sleep(0.3) + else: + time.sleep(0.05) + + def _capturer(self): + if self.sim: + time.sleep(1.5) + img=Image.new("RGB",(W,H),color=(80,60,100)) + nom=f"photo_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}.jpg" + ch=str(PHOTOS_DIR/nom); img.save(ch,"JPEG",quality=85); return ch + self.cap_result=None; self.cap_done.clear(); self.cap_demande=True + if not self.cap_done.wait(timeout=15): self.cap_demande=False + return self.cap_result + + # โ”€โ”€ Sรฉquence photos โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _sequence(self): + if self.mode == "simple": + nb = 1 + elif self.format == "bande3x2": + nb = 3 + else: # grille_4 et bande4x2 + nb = 4 + delai = self.cfg.get("camera",{}).get("compte_a_rebours",3) + self.photos=[]; self.gal_surfs=[]; self.gal_ready=False; self.session_qr=None; self.session_url=None + + for i in range(nb): + self.num_ph=i; self.etat=S_PREV; time.sleep(1.5) + self.etat=S_COMPTE + for c in range(delai,0,-1): self.chiffre=c; time.sleep(1) + self.etat=S_CAP; ch=self._capturer() + if ch: self.photos.append(ch); self.compteur+=1; self._sav_cpt() + time.sleep(0.4) + if not self.photos: self.etat=S_ACCUEIL; return + self._upload_booth_bg(self.photos) # upload + QR session en arriรจre-plan + if self.mode=="simple": + self.photo_active=self.photos[0]; self.share_msg=""; self.etat=S_SHARE + else: + threading.Thread(target=self._load_gal,daemon=True).start() + self.etat=S_GAL + + def _load_gal(self): + surfs = [] + if self.format in ("bande3x2","bande4x2"): + nb = 3 if self.format=="bande3x2" else 4 + pw = W // nb; sh = (H - 150) // 2 + for ch in self.photos[:nb]: + try: + img=ImageOps.exif_transpose(Image.open(ch).convert("RGB")).resize((pw,sh),Image.LANCZOS) + surfs.append(pygame.image.fromstring(img.tobytes(),img.size,"RGB")) + except: surfs.append(None) + else: # grille_4 + gw,gh=W//2-15,H//2-75 + for ch in self.photos[:4]: + try: + img=Image.open(ch).convert("RGB").resize((gw,gh),Image.LANCZOS) + surfs.append(pygame.image.fromstring(img.tobytes(),img.size,"RGB")) + except: surfs.append(None) + self.gal_surfs=surfs; self.gal_ready=True + + def _sav_cpt(self): + try: + c=cfg_load(); c.setdefault("compteur",{})["photos_prises"]=self.compteur; cfg_save(c) + except: pass + + def _upload_booth(self, photos): + """Upload photos vers photobooth.copydev.fr/api/sessions/{code}/upload.""" + b = self.cfg.get("booth", {}) + url = b.get("url", "https://photobooth.copydev.fr").rstrip("/") + code = b.get("session_code", "") + if not code: return None + from urllib.request import Request, urlopen + ok = 0 + for ch in photos: + try: + boundary = "----PBUpload" + with open(ch, "rb") as f: data = f.read() + nom = Path(ch).name + body = (f"--{boundary}\r\nContent-Disposition: form-data; name=\"fichier\"; filename=\"{nom}\"\r\nContent-Type: image/jpeg\r\n\r\n").encode() + data + f"\r\n--{boundary}--\r\n".encode() + req = Request(f"{url}/api/sessions/{code}/upload", data=body, method="POST") + req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}") + with urlopen(req, timeout=10) as r: + if r.status == 200: ok += 1 + except Exception as e: + print(f"booth upload: {e}") + return f"{url}/s/{code}" if ok > 0 else None + + def _upload_booth_bg(self, photos): + """Lance l'upload booth en arriรจre-plan et met ร  jour le QR de session.""" + def _do(): + sess_url = self._upload_booth(photos) + if sess_url and HAS_QR: + try: + img = qrlib.make(sess_url, border=2).convert("RGB").resize((200,200), Image.NEAREST) + self.session_qr = pygame.image.fromstring(img.tobytes(), img.size, "RGB") + self.session_url = sess_url + print(f"Session booth: {sess_url}") + except Exception as e: + print(f"booth QR: {e}") + threading.Thread(target=_do, daemon=True).start() + + def _imprimer(self, ch, copies=1): + if not HAS_CUPS or not ch: return False + try: + conn=cups.Connection(); ps=conn.getPrinters() + nom=self.cfg.get("impression",{}).get("imprimante") or (list(ps.keys())[0] if ps else None) + if not nom: return False + conn.printFile(nom,ch,"Photobooth",{"copies":str(copies),"fit-to-page":"True"}); return True + except Exception as e: print(f"print: {e}"); return False + + def _email_send(self, dest, ch): + ce=self.cfg.get("email",{}); nom=self.cfg.get("evenement",{}).get("nom","Photobooth") + try: + msg=MIMEMultipart(); msg["From"]=ce.get("expediteur",""); msg["To"]=dest + msg["Subject"]=ce.get("sujet","Votre photo").replace("{evenement}",nom) + msg.attach(MIMEText(ce.get("message","Voici votre photo !").replace("{evenement}",nom),"plain")) + with open(ch,"rb") as f: + p=MIMEBase("application","octet-stream"); p.set_payload(f.read()) + encoders.encode_base64(p) + p.add_header("Content-Disposition",f'attachment; filename="{Path(ch).name}"') + msg.attach(p) + s=smtplib.SMTP(ce.get("smtp_host",""),ce.get("smtp_port",587)); s.starttls() + s.login(ce.get("smtp_user",""),ce.get("smtp_password","")) + s.sendmail(msg["From"],[dest],msg.as_string()); s.quit(); return True + except Exception as e: print(f"email: {e}"); return False + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # ร‰CRANS + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + + # โ”€โ”€ Accueil โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def draw_accueil(self): + self._bg(); t=time.time()-self.t0 + y=70; T=self.T + + logo=self._logo() + if logo: self._c(logo,y); y+=logo.get_height()+30 + else: + s=self._txt("Photobooth","xl",T["prim"]); self._c(s,y); y+=s.get_height()+30 + + nom=self.cfg.get("evenement",{}).get("nom","") or "Photobooth" + s=self._txt(nom,"lg",T["prim"]); self._c(s,y); y+=s.get_height()+50 + + # Instruction pulsante + a=int(140+115*math.sin(t*math.pi)) + ins=self._txt("Touchez pour commencer","sm",T["texte2"],False); ins.set_alpha(a) + self._c(ins,y); y+=ins.get_height()+55 + + # Cercle animรฉ + r1=int(50+20*math.sin(t*math.pi)); r2=int(80+25*math.sin(t*math.pi+0.5)) + cs=pygame.Surface((240,240),pygame.SRCALPHA) + a2=int(25+25*abs(math.sin(t*math.pi))) + pygame.draw.circle(cs,(*T["prim"],a2),(120,120),r2) + pygame.draw.circle(cs,(*T["prim"],70),(120,120),r1) + pygame.draw.circle(cs,(*T["prim"],255),(120,120),16) + self._c(cs,y) + + # QR bas-gauche + if self.qr_surf: + qy=H-self.qr_surf.get_height()-55 + self.scr.blit(self.qr_surf,(20,qy)) + + # Compteur + if self.cfg.get("compteur",{}).get("actif"): + s=self._txt(f"{max(0,self.cpt_max-self.compteur)} / {self.cpt_max} photos restantes","xxs",T["texte2"],False) + self.scr.blit(s,(20,H-36)) + + # Bouton admin โš™ (discret mais visible) + r_adm=pygame.Rect(W-80,H-80,66,66) + pygame.draw.circle(self.scr,T["carte"],(r_adm.centerx,r_adm.centery),33) + pygame.draw.circle(self.scr,T["prim"],(r_adm.centerx,r_adm.centery),33,2) + sg=self.F["md"].render("โš™",True,T["prim"]); self.scr.blit(sg,(r_adm.centerx-sg.get_width()//2,r_adm.centery-sg.get_height()//2)) + + # โ”€โ”€ Mode โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def draw_mode(self): + self._bg(); T=self.T + s=self._txt("Choisissez votre mode","lg",T["texte"]); self._c(s,60) + + feat=self.cfg.get("fonctionnalites",{}) + btns={}; cw=780; ch=680; gap=60 + tx=(W-(2*cw+gap))//2; ty=190 + + infos=[("simple","photo_simple","Photo","1 prise","[ ]"), + ("multi","multi_shot","Pellicule", + f'{self.cfg.get("multi_shot",{}).get("nombre_photos",4)} poses',"[|||]")] + + for idx,(key,fk,titre,sous,ic) in enumerate(infos): + if not feat.get(fk,True): continue + x=tx+idx*(cw+gap); r=pygame.Rect(x,ty,cw,ch) + self._card(r); + s=self.F["xl"].render(ic,True,T["prim"]) + self.scr.blit(s,(r.centerx-s.get_width()//2,ty+70)) + s=self._txt(titre,"lg",T["texte"]); self.scr.blit(s,(r.centerx-s.get_width()//2,ty+310)) + s=self._txt(sous,"sm",T["texte2"]); self.scr.blit(s,(r.centerx-s.get_width()//2,ty+420)) + btns[key]=r + + r_ret=pygame.Rect(W//2-210,H-105,420,72) + self._btn(r_ret,T["carte"],"Retour","sm",tc=T["texte"]) + btns["retour"]=r_ret + return btns + + # โ”€โ”€ Format multipose โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _draw_format_preview(self, x, y, w, h, fmt): + T=self.T; c=T["prim"]; c2=(*T["prim"],80) + if fmt=="grille_4": + gw,gh=w//2-4,h//2-4 + for row in range(2): + for col in range(2): + pygame.draw.rect(self.scr,c,(x+col*(gw+4),y+row*(gh+4),gw,gh),border_radius=4) + elif fmt=="bande3": + # 2 bandes horizontales empilรฉes, 3 photos cรดte ร  cรดte par bande + sh=h//2-4; fw=w//3-3 + for s in range(2): + sy=y+s*(sh+4) + for i in range(3): + pygame.draw.rect(self.scr,c,(x+i*(fw+3),sy,fw,sh),border_radius=3) + elif fmt=="bande4": + # 2 bandes horizontales empilรฉes, 4 photos cรดte ร  cรดte par bande + sh=h//2-4; fw=w//4-2 + for s in range(2): + sy=y+s*(sh+4) + for i in range(4): + pygame.draw.rect(self.scr,c,(x+i*(fw+2),sy,fw,sh),border_radius=3) + + def draw_format(self): + self._bg(); T=self.T; btns={} + s=self._txt("Choisissez votre mise en page","lg",T["texte"]); self._c(s,55) + + specs=[ + ("grille_4", "Grille 2ร—2", "4 poses ยท collage carrรฉ", "grille_4"), + ("bande3x2", "Bande ร— 2", "3 poses ยท bande doublรฉe", "bande3"), + ("bande4x2", "Bande ร— 2", "4 poses ยท bande doublรฉe", "bande4"), + ] + cw=460; ch=560; gap=55 + tx=(W-(3*cw+2*gap))//2; ty=160 + + for idx,(key,titre,desc,pfmt) in enumerate(specs): + x=tx+idx*(cw+gap); r=pygame.Rect(x,ty,cw,ch) + sel=(key==self.format) + bord=T["prim"] if sel else (80,80,110) + self._card(r,bord=bord,bord_w=4 if sel else 2) + + # Aperรงu dessinรฉ + pw,ph=cw-80,240; px=x+40; py=ty+30 + bg2=pygame.Surface((pw,ph),pygame.SRCALPHA); bg2.fill((*T["fond2"],180)) + self.scr.blit(bg2,(px,py)) + self._draw_format_preview(px+10,py+10,pw-20,ph-20,pfmt) + + sn=self._txt(titre,"md",T["texte"]) + self.scr.blit(sn,(r.centerx-sn.get_width()//2,ty+300)) + sd=self._txt(desc,"xs",T["texte2"],False) + self.scr.blit(sd,(r.centerx-sd.get_width()//2,ty+380)) + if sel: + sc=self.F["xs"].render("โœ“ Sรฉlectionnรฉ",True,T["prim"]) + self.scr.blit(sc,(r.centerx-sc.get_width()//2,ty+460)) + btns[key]=r + + r_ret=pygame.Rect(W//2-210,H-105,420,72) + self._btn(r_ret,T["carte"],"Retour","sm",tc=T["texte"]); btns["retour"]=r_ret + return btns + + # โ”€โ”€ Preview / Compte / Capture โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def draw_preview(self): + self._lv() + bande=pygame.Surface((W,80),pygame.SRCALPHA); bande.fill((0,0,0,170)) + self.scr.blit(bande,(0,H-80)) + nb=len(self.photos)+1 + total=1 if self.mode=="simple" else (3 if self.format=="bande3x2" else 4) + s=self.F["sm"].render(f"Photo {nb} / {total} โ€” Souriez !",True,(255,255,255)) + self.scr.blit(s,(W//2-s.get_width()//2,H-62)) + + def draw_compte(self): + self._lv(); self._overlay(110) + ch=str(self.chiffre); T=self.T + s=self.F["xxl"].render(ch,True,(255,255,255)) + hr=s.get_width()//2+90 + hs=pygame.Surface((hr*2,hr*2),pygame.SRCALPHA) + for i in range(5): + a=int(50-i*8) + pygame.draw.circle(hs,(*T["prim"],a),(hr,hr),hr-i*16) + self.scr.blit(hs,(W//2-hr,H//2-hr)) + self._c(s,H//2-s.get_height()//2) + total=1 if self.mode=="simple" else (3 if self.format=="bande3x2" else 4) + info=self.F["sm"].render(f"Photo {self.num_ph+1} / {total}",True,T["texte2"]) + self.scr.blit(info,(W//2-info.get_width()//2,H-90)) + + def draw_capture(self): + self._lv(); self._overlay(90) + s=self._txt("Prise de vue...","md"); self._c(s,H//2-s.get_height()//2) + + # โ”€โ”€ Galerie โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def draw_galerie(self): + self._bg(); T=self.T + + if not self.gal_ready: + t=time.time() + s=self._txt("Chargement des photos...","sm",T["texte2"],False); self._c(s,H//2-50) + for k in range(4): + a=int(128+127*math.sin(t*3+k*1.5)) + pygame.draw.circle(self.scr,(*T["prim"],a),(W//2-60+k*40,H//2+30),12) + elif self.format in ("bande3x2","bande4x2"): + nb=3 if self.format=="bande3x2" else 4 + pw=W//nb; sh=(H-150)//2 + for strip_y in (5, sh+10): + for i,surf in enumerate(self.gal_surfs[:nb]): + rx=i*pw + if surf: + self.scr.blit(surf,(rx,strip_y)) + pygame.draw.rect(self.scr,T["prim"],pygame.Rect(rx,strip_y,pw,sh),2) + # Sรฉparateur horizontal central + pygame.draw.line(self.scr,T["carte"],(0,sh+7),(W,sh+7),2) + else: # grille_4 + gw,gh=W//2-15,H//2-75 + pos=[(5,5),(W//2+5,5),(5,H//2-65),(W//2+5,H//2-65)] + for i,surf in enumerate(self.gal_surfs[:4]): + x,y=pos[i] + if surf: + self.scr.blit(surf,(x,y)) + pygame.draw.rect(self.scr,T["prim"],pygame.Rect(x,y,gw,gh),2) + else: + pygame.draw.rect(self.scr,T["carte"],pygame.Rect(x,y,gw,gh)) + + bande=pygame.Surface((W,120),pygame.SRCALPHA); bande.fill((0,0,0,200)) + self.scr.blit(bande,(0,H-120)) + btns={}; feat=self.cfg.get("fonctionnalites",{}) + bx=50; bw,bh=260,88; by=H-110 + + for key,label in [("imprimer","Imprimer"),("email","Email")]: + fk={"imprimer":"impression","email":"email"}[key] + if feat.get(fk,True): + r=pygame.Rect(bx,by,bw,bh); self._btn(r,T["carte"],label,"sm",tc=T["texte"]); btns[key]=r; bx+=bw+20 + + r=pygame.Rect(W-bw-50,by,bw,bh); self._btn(r,T["prim"],"Nouvelle session","sm"); btns["nouveau"]=r + + # QR session โ€” mรชme QR que l'accueil (galerie de l'รฉvรฉnement) + qr = self.qr_surf + if qr: + qw=qr.get_width(); qh=qr.get_height() + qx=W-qw-15; qy=5 + bg=pygame.Surface((qw+20,qh+40),pygame.SRCALPHA); bg.fill((0,0,0,180)) + self.scr.blit(bg,(qx-10,qy)) + self.scr.blit(qr,(qx,qy+20)) + sl=self.F["xs"].render("Voir la galerie",True,(255,255,255)) + self.scr.blit(sl,(qx+qw//2-sl.get_width()//2,qy+4)) + # Indicateur upload en cours + if not self.session_qr and self.gal_ready: + sl=self.F["xs"].render("โฌ† Upload...",True,T["texte2"]) + self.scr.blit(sl,(W-sl.get_width()-20,12)) + + return btns + + # โ”€โ”€ Partage โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def draw_partage(self): + self._bg(); T=self.T + s=self._txt("Partagez votre photo !","lg",T["texte"]); self._c(s,50) + + if self.photo_active and Path(self.photo_active).exists(): + try: + img=Image.open(self.photo_active).convert("RGB"); img.thumbnail((680,460),Image.LANCZOS) + surf=pygame.image.fromstring(img.tobytes(),img.size,"RGB") + px=W//2-surf.get_width()//2; py=160 + pygame.draw.rect(self.scr,(0,0,0),pygame.Rect(px+6,py+8,surf.get_width(),surf.get_height()),border_radius=4) + self.scr.blit(surf,(px,py)) + pygame.draw.rect(self.scr,T["prim"],pygame.Rect(px,py,surf.get_width(),surf.get_height()),2,border_radius=4) + except: pass + + feat=self.cfg.get("fonctionnalites",{}); btns={} + acts=[("imprimer","impression","Imprimer"),("email","email","Email"),("qr","qr_code","QR Code")] + avail=[a for a in acts if feat.get(a[1],True)] + bw=260; gap=40; bh=170 + bx=W//2-(len(avail)*bw+(len(avail)-1)*gap)//2; by=660 + + for key,_,label in avail: + r=pygame.Rect(bx,by,bw,bh); self._card(r) + s=self._txt(label,"sm",T["texte"]); self.scr.blit(s,(r.centerx-s.get_width()//2,r.centery-s.get_height()//2)) + btns[key]=r; bx+=bw+gap + + if self.share_msg: + col=(76,175,80) if "OK" in self.share_msg else (244,67,54) + s=self._txt(self.share_msg,"sm",col,False); self._c(s,by+bh+24) + + r_fin=pygame.Rect(W//2+50,H-105,300,76); self._btn(r_fin,T["prim"],"Terminer","sm"); btns["terminer"]=r_fin + r_nw=pygame.Rect(W//2-350,H-105,300,76); self._btn(r_nw,T["carte"],"Nouvelle photo","sm",tc=T["texte"]); btns["nouveau"]=r_nw + return btns + + # โ”€โ”€ Email clavier โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def draw_email(self): + self._bg(); T=self.T + s=self._txt("Entrez votre adresse email","lg",T["texte"]); self._c(s,50) + + field=pygame.Rect(W//2-520,150,1040,82) + self._card(field,rayon=14) + buf=self.F["md"].render(self.email_buf or " ",True,T["texte"]) + self.scr.blit(buf,(field.x+20,field.centery-buf.get_height()//2)) + + lignes=[list("1234567890"),list("azertyuiop"),list("qsdfghjklm"),list("wxcvbn@.-_")] + extras=[("<--",120),("@gmail.com",280),("@hotmail.fr",300),(".com",150),(".fr",120)] + btns={}; ky=278; kw,kh=88,76; gap=7 + + for ligne in lignes: + kx=W//2-(len(ligne)*(kw+gap)-gap)//2 + for ch in ligne: + r=pygame.Rect(kx,ky,kw,kh); self._btn(r,T["carte"],ch,"sm",12,tc=T["texte"]); btns[("k",ch)]=r; kx+=kw+gap + ky+=kh+gap + + ky+=8; kx=W//2-(sum(ew+gap for _,ew in extras)-gap)//2 + for ex,ew in extras: + r=pygame.Rect(kx,ky,ew,kh) + col=(244,67,54) if ex=="<--" else T["carte"] + self._btn(r,col,ex,"xs",12,tc=T["texte"] if ex!="<--" else (255,255,255)) + btns[("k",ex)]=r; kx+=ew+gap + + ky+=kh+22 + rs=pygame.Rect(W//2-340,ky,310,76); self._btn(rs,T["prim"],"Envoyer","sm"); btns["envoyer"]=rs + ra=pygame.Rect(W//2+30,ky,310,76); self._btn(ra,T["carte"],"Annuler","sm",tc=T["texte"]); btns["annuler"]=ra + + if self.email_msg: + col=(76,175,80) if "OK" in self.email_msg else (244,67,54) + s=self._txt(self.email_msg,"sm",col,False); self._c(s,ky+90) + return btns + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # WIZARD + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + def draw_wizard(self): + self._bg(); T=self.T; btns={} + STEPS=7 + # Barre de progression + self._progress_bar(self.wiz_step,STEPS,y=30) + # Fermer + rc=pygame.Rect(W-90,10,70,70); self._btn(rc,T["carte"],"X","md",35,tc=T["texte"]); btns["fermer"]=rc + + step=self.wiz_step + + # โ”€โ”€ ร‰tape 1 : Nom de l'รฉvรฉnement โ”€โ”€ + if step==1: + s=self._txt("Nom de l'evenement","lg",T["texte"]); self._c(s,80) + s=self._txt("Mariage, anniversaire, soiree...","sm",T["texte2"],False); self._c(s,185) + + field=pygame.Rect(W//2-520,255,1040,82); self._card(field,rayon=14) + buf=self.F["md"].render(self.wiz_kbbuf or " ",True,T["texte"]) + self.scr.blit(buf,(field.x+20,field.centery-buf.get_height()//2)) + + lignes=[list("1234567890 "),list("azertyuiop"),list("qsdfghjklm"),list("wxcvbn-'& ")] + extras=[("<--",120),("Espace",200),(".com",150),(".fr",120)] + ky=370; kw,kh=84,72; gap=7 + + for ligne in lignes: + kx=W//2-(len(ligne)*(kw+gap)-gap)//2 + for ch in ligne: + lbl=" " if ch==" " else ch + r=pygame.Rect(kx,ky,kw,kh); self._btn(r,T["carte"],lbl,"sm",12,tc=T["texte"]); btns[("k",ch)]=r; kx+=kw+gap + ky+=kh+gap + + ky+=6; kx=W//2-(sum(ew+gap for _,ew in extras)-gap)//2 + for ex,ew in extras: + r=pygame.Rect(kx,ky,ew,kh) + col=(244,67,54) if ex=="<--" else T["carte"] + self._btn(r,col,ex,"xs",12,tc=T["texte"] if ex!="<--" else (255,255,255)) + btns[("k",ex)]=r; kx+=ew+gap + + ky+=kh+24 + rn=pygame.Rect(W//2-210,ky,420,76); self._btn(rn,T["prim"],"Suivant","sm"); btns["next"]=rn + + # โ”€โ”€ ร‰tape 2 : Code de session galerie โ”€โ”€ + elif step==2: + s=self._txt("Code de session galerie","lg",T["texte"]); self._c(s,80) + s2=self._txt("Les photos de cet evenement seront accessibles via ce code","sm",T["texte2"],False); self._c(s2,160) + + # Affichage du code (9 chiffres) โ€” fond carte pour isoler visuellement + c=self.wiz_code_buf + if len(c)==9: code_disp=f"{c[:3]}-{c[3:6]}-{c[6:]}" + elif len(c)>0: code_disp=c + else: code_disp="โ€”" + code_card=pygame.Rect(W//2-400,195,800,100); self._card(code_card) + sc=self.F["lg"].render(code_disp,True,T["prim"]) # lg=96px, no overlap + self.scr.blit(sc,(code_card.centerx-sc.get_width()//2, code_card.centery-sc.get_height()//2)) + + # Statut du test + if self.wiz_code_status=="ok": + st_col=(76,175,80); st_txt="Connexion OK โ€” session prete" + elif self.wiz_code_status=="erreur": + st_col=(244,67,54); st_txt="Erreur de connexion" + elif self.wiz_code_status=="test...": + st_col=(255,193,7); st_txt="Test en cours..." + else: + st_col=T["texte2"]; st_txt="Appuyez sur Tester pour verifier" + ss=self.F["xs"].render(st_txt,True,st_col); self._c(ss,312) + + # Pavรฉ numรฉrique compact โ€” dรฉmarre ร  y=360 + nkeys=["1","2","3","4","5","6","7","8","9","โŒซ","0","OK"] + nkw,nkh,nkg=150,100,14 + ngrid_w=3*nkw+2*nkg; tx2=W//2-ngrid_w//2; ty2=364 + for i,k in enumerate(nkeys): + col_i=i%3; row_i=i//3 + r=pygame.Rect(tx2+col_i*(nkw+nkg),ty2+row_i*(nkh+nkg),nkw,nkh) + if k=="OK": cb=(76,175,80); tc2=(255,255,255) + elif k=="โŒซ": cb=(244,67,54); tc2=(255,255,255) + else: cb=T["carte"]; tc2=T["texte"] + self._btn(r,cb,k,"md",tc=tc2); btns[("code",k)]=r + # Pavรฉ bottom = 364 + 4*(100+14) - 14 = 364 + 442 = 806 + + # Boutons action : Gรฉnรฉrer | Tester | Suivant + rg=pygame.Rect(W//2-700,H-155,360,72); self._btn(rg,(80,80,130),"Generer nouveau","xs",tc=(255,255,255)); btns["code_gen"]=rg + rt=pygame.Rect(W//2-320,H-155,360,72) + t_col=(80,80,130) if len(self.wiz_code_buf)!=9 else T["prim"] + self._btn(rt,t_col,"Tester connexion","xs",tc=(255,255,255)); btns["code_test"]=rt + rn=pygame.Rect(W//2+60,H-155,360,72); self._btn(rn,T["prim"],"Suivant","sm"); btns["next"]=rn + rpv=pygame.Rect(W//2-700,H-70,340,60); self._btn(rpv,T["carte"],"Retour","xs",tc=T["texte"]); btns["prev"]=rpv + + # โ”€โ”€ ร‰tape 3 : Thรจme โ”€โ”€ + elif step==3: + s=self._txt("Theme visuel","lg",T["texte"]); self._c(s,80) + tlist=[ + ("base", "Sombre", (26,26,46), (233,30,99), (255,255,255)), + ("photobooth", "Photobooth", (235,242,255),(25,100,230), (15,30,80)), + ("sikapics", "SikaPics", (251,231,232),(195,31,72), (36,4,57)), + ("copydev", "CopyDev", (8,18,40), (41,182,246), (255,255,255)), + ] + cw=360; ch_h=460; gap=36; ty=180 + tx=(W-(len(tlist)*cw+(len(tlist)-1)*gap))//2 + sel=self.wiz_data.get("theme","base") + + for idx,(tkey,tnom,bg,prim,tex) in enumerate(tlist): + x=tx+idx*(cw+gap); r=pygame.Rect(x,ty,cw,ch_h) + bord=prim if tkey==sel else (100,100,120) + bw=4 if tkey==sel else 2 + self._card(r,bord=bord,fond=bg,bord_w=bw) + # Preview mini + preview=pygame.Rect(x+30,ty+30,cw-60,200) + pygame.draw.rect(self.scr,bg,preview,border_radius=12) + # Cercle d'accentuation + pygame.draw.circle(self.scr,prim,(preview.centerx,preview.centery),50) + pygame.draw.circle(self.scr,tex,(preview.centerx,preview.centery),20) + # Nom + st=self.F["sm"].render(tnom,True,tex if tkey=="sikapics" else (255,255,255)) + self.scr.blit(st,(r.centerx-st.get_width()//2,ty+260)) + # Coche si sรฉlectionnรฉ + if tkey==sel: + sc=self.F["md"].render("[OK]",True,prim) + self.scr.blit(sc,(r.centerx-sc.get_width()//2,ty+340)) + btns[("theme",tkey)]=r + + rn=pygame.Rect(W//2+50,H-105,300,76); self._btn(rn,T["prim"],"Suivant","sm"); btns["next"]=rn + rp=pygame.Rect(W//2-350,H-105,300,76); self._btn(rp,T["carte"],"Retour","sm",tc=T["texte"]); btns["prev"]=rp + + # โ”€โ”€ ร‰tape 4 : Modes photo โ”€โ”€ + elif step==4: + s=self._txt("Modes photo","lg",T["texte"]); self._c(s,80) + modes=[("photo_simple","Photo","1 prise"),("multi_shot","Pellicule","Plusieurs poses")] + cw=520; gap=80; ty=210 + tx=(W-(2*cw+gap))//2 + feat=self.wiz_data.get("modes",{"photo_simple":True,"multi_shot":True}) + + for idx,(key,nom,desc) in enumerate(modes): + x=tx+idx*(cw+gap); r=pygame.Rect(x,ty,cw,380) + actif=feat.get(key,True) + bord=T["prim"] if actif else (80,80,100) + self._card(r,bord=bord) + ic=self.F["xl"].render("[ ]" if key=="photo_simple" else "[|||]",True,T["prim"] if actif else T["texte2"]) + self.scr.blit(ic,(r.centerx-ic.get_width()//2,ty+40)) + sn=self._txt(nom,"md",T["texte"] if actif else T["texte2"]) + self.scr.blit(sn,(r.centerx-sn.get_width()//2,ty+230)) + sd=self._txt(desc,"xs",T["texte2"],False) + self.scr.blit(sd,(r.centerx-sd.get_width()//2,ty+305)) + if actif: + sa=self.F["xs"].render("[Actif]",True,T["prim"]) + self.scr.blit(sa,(r.centerx-sa.get_width()//2,ty+340)) + btns[("mode",key)]=r + + rn=pygame.Rect(W//2+50,H-105,300,76); self._btn(rn,T["prim"],"Suivant","sm"); btns["next"]=rn + rp=pygame.Rect(W//2-350,H-105,300,76); self._btn(rp,T["carte"],"Retour","sm",tc=T["texte"]); btns["prev"]=rp + + # โ”€โ”€ ร‰tape 5 : Nombre de photos โ”€โ”€ + elif step==5: + s=self._txt("Photos par pellicule","lg",T["texte"]); self._c(s,80) + nb=self.wiz_data.get("nb_photos",4) + card=pygame.Rect(W//2-200,200,400,300); self._card(card) + sn=self.F["xxl"].render(str(nb),True,T["prim"]) + self.scr.blit(sn,(card.centerx-sn.get_width()//2,card.centery-sn.get_height()//2)) + + rm=pygame.Rect(W//2-360,H//2-60,140,140); self._btn(rm,T["carte"],"-","xl",70,tc=T["texte"]); btns["moins"]=rm + rp2=pygame.Rect(W//2+220,H//2-60,140,140); self._btn(rp2,T["carte"],"+","xl",70,tc=T["texte"]); btns["plus"]=rp2 + + rn=pygame.Rect(W//2+50,H-105,300,76); self._btn(rn,T["prim"],"Suivant","sm"); btns["next"]=rn + rpv=pygame.Rect(W//2-350,H-105,300,76); self._btn(rpv,T["carte"],"Retour","sm",tc=T["texte"]); btns["prev"]=rpv + + # โ”€โ”€ ร‰tape 6 : Partage โ”€โ”€ + elif step==6: + s=self._txt("Options de partage","lg",T["texte"]); self._c(s,80) + opts=[("impression","Imprimer"),("email","Email"),("qr_code","QR Code"),("galerie","Galerie")] + share=self.wiz_data.get("share",{o:True for o,_ in opts}) + cw=350; gap=40; ty=210 + tx=(W-(2*cw+gap))//2 + + for idx,(key,nom) in enumerate(opts): + row,col=divmod(idx,2); x=tx+col*(cw+gap); y2=ty+row*(240+gap) + r=pygame.Rect(x,y2,cw,220) + actif=share.get(key,True); bord=T["prim"] if actif else (80,80,100) + self._card(r,bord=bord) + sn=self._txt(nom,"sm",T["texte"] if actif else T["texte2"]) + self.scr.blit(sn,(r.centerx-sn.get_width()//2,r.centery-sn.get_height()//2)) + if actif: + sa=self.F["xs"].render("[ON]",True,T["prim"]) + self.scr.blit(sa,(r.centerx-sa.get_width()//2,r.y+160)) + btns[("share",key)]=r + + rn=pygame.Rect(W//2+50,H-105,300,76); self._btn(rn,T["prim"],"Suivant","sm"); btns["next"]=rn + rpv=pygame.Rect(W//2-350,H-105,300,76); self._btn(rpv,T["carte"],"Retour","sm",tc=T["texte"]); btns["prev"]=rpv + + # โ”€โ”€ ร‰tape 7 : Rรฉsumรฉ / Lancer โ”€โ”€ + elif step==7: + s=self._txt("C'est pret !","xl",T["prim"]); self._c(s,80) + _c=self.wiz_data.get("session_code",self.wiz_code_buf) or "โ€”" + if len(_c)==9: _c=f"{_c[:3]}-{_c[3:6]}-{_c[6:]}" + resume=[ + ("Evenement", self.wiz_data.get("nom","โ€”")), + ("Code galerie", _c), + ("Theme", THEMES.get(self.wiz_data.get("theme","base"),{}).get("nom","โ€”")), + ("Modes", ", ".join(k for k,v in self.wiz_data.get("modes",{}).items() if v) or "โ€”"), + ("Photos/pellicule", str(self.wiz_data.get("nb_photos",4))), + ("Partage", ", ".join(k for k,v in self.wiz_data.get("share",{}).items() if v) or "โ€”"), + ] + card=pygame.Rect(W//2-450,190,900,540); self._card(card) + ry=215 + for label,val in resume: + sl=self.F["xs"].render(label+" :",True,T["texte2"]) + sv=self.F["sm"].render(val,True,T["texte"]) + self.scr.blit(sl,(card.x+40,ry)) + self.scr.blit(sv,(card.x+280,ry)); ry+=66 + + rl=pygame.Rect(W//2-310,H-105,620,80); self._btn(rl,T["prim"],"Lancer le photobooth","md"); btns["lancer"]=rl + rpv=pygame.Rect(W//2-350,H-195,300,72); self._btn(rpv,T["carte"],"Retour","sm",tc=T["texte"]); btns["prev"]=rpv + + return btns + + # โ”€โ”€ PIN โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def draw_pin(self): + self._bg(); T=self.T; btns={} + s=self._txt("โš™ Administration","xl",T["texte"]); self._c(s,70) + s=self._txt("Entrez le code d'accรจs","sm",T["texte2"],False); self._c(s,185) + # Affichage PIN + dots="โ—"*len(self.pin_buf)+"โ—‹"*(4-len(self.pin_buf)) + sd=self.F["lg"].render(dots,True,T["prim"]); self._c(sd,228) + if self.pin_err: + se=self._txt("Code incorrect","sm",(244,67,54),False); self._c(se,318) + # Pavรฉ numรฉrique โ€” grandes touches centrรฉes + keys=["1","2","3","4","5","6","7","8","9","โŒซ","0","OK"] + kw,kh,kg=220,150,22 + grid_w=3*kw+2*kg; grid_h=4*kh+3*kg + tx=W//2-grid_w//2; ty=H//2-grid_h//2+30 + for i,k in enumerate(keys): + col=i%3; row=i//3 + r=pygame.Rect(tx+col*(kw+kg), ty+row*(kh+kg), kw, kh) + if k=="OK": cb=T["prim"]; tc=(255,255,255) + elif k=="โŒซ": cb=(100,50,80); tc=(255,255,255) + else: cb=T["carte"]; tc=T["texte"] + self._btn(r,cb,k,"lg",tc=tc); btns[("pin",k)]=r + rc=pygame.Rect(W//2-210,H-95,420,72); self._btn(rc,T["carte"],"Annuler","sm",tc=T["texte"]); btns["annuler"]=rc + return btns + + # โ”€โ”€ Admin (tabbed) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def draw_admin(self): + self._bg(); T=self.T; btns={} + s=self._txt("Administration","sm",T["texte"]); self._c(s,20) + # Onglets + tw=268; tgap=12; ty=84; th=58 + tx=(W-(len(ADMIN_TABS)*tw+(len(ADMIN_TABS)-1)*tgap))//2 + for i,(key,label) in enumerate(ADMIN_TABS): + r=pygame.Rect(tx+i*(tw+tgap),ty,tw,th) + sel=(key==self.admin_tab) + col=T["prim"] if sel else T["carte"] + tc2=(255,255,255) if sel or not T["clair"] else T["texte"] + self._btn(r,col,label,"xs",tc=tc2); btns[("tab",key)]=r + pygame.draw.line(self.scr,T["prim"],(40,155),(W-40,155),2) + cy=168 + if self.admin_tab=="fonctions": self._adm_fonctions(btns,cy) + elif self.admin_tab=="materiel": self._adm_materiel(btns,cy) + elif self.admin_tab=="compteur": self._adm_compteur(btns,cy) + elif self.admin_tab=="evenement": self._adm_evenement(btns,cy) + elif self.admin_tab=="systeme": self._adm_systeme(btns,cy) + # Bas + rw=pygame.Rect(W//2-360,H-105,340,72); self._btn(rw,T["prim"],"Wizard รฉvรฉnement","sm"); btns["wizard"]=rw + rc=pygame.Rect(W//2+20,H-105,340,72); self._btn(rc,T["carte"],"Retour","sm",tc=T["texte"]); btns["retour"]=rc + return btns + + def _tog_row(self,btns,key,label,actif,x,y,w=860): + T=self.T; r=pygame.Rect(x,y,w,64); self._card(r,bord=T["prim"] if actif else (80,80,110),rayon=12) + sl=self.F["sm"].render(label,True,T["texte"]); self.scr.blit(sl,(r.x+22,r.centery-sl.get_height()//2)) + c=(76,175,80) if actif else (244,67,54); tog=pygame.Rect(r.right-120,r.y+12,100,40) + self._btn(tog,c,"ON" if actif else "OFF","xs"); btns[("t",key)]=tog + + def _adm_fonctions(self,btns,y): + feat=self.cfg.get("fonctionnalites",{}) + items=[("photo_simple","Photo simple"),("multi_shot","Pellicule"), + ("impression","Impression"), ("email","Email"), + ("qr_code","QR Code"), ("filtres","Filtres"), + ("overlays","Overlays"), ("chroma_key","Chroma key"), + ("galerie","Galerie web"), ("compteur_feat","Compteur")] + cw=860; gap=28; rh=64 + lx=W//2-cw-gap//2; rx=W//2+gap//2 + for i,(key,label) in enumerate(items): + col=i%2; row=i//2 + x=lx if col==0 else rx + self._tog_row(btns,key,label,feat.get(key,True),x,y+row*(rh+10),cw) + + def _adm_materiel(self,btns,y): + T=self.T; cc=self.cfg.get("camera",{}); ci=self.cfg.get("impression",{}) + def pm_card(title,val,key_m,key_p,x,yy,w=430,h=140): + r=pygame.Rect(x,yy,w,h); self._card(r) + s=self._txt(title,"xs",T["texte2"],False); self.scr.blit(s,(r.x+16,yy+12)) + sv=self.F["lg"].render(str(val),True,T["prim"]); self.scr.blit(sv,(r.centerx-sv.get_width()//2,yy+44)) + rm=pygame.Rect(r.x+8,yy+44,90,56); self._btn(rm,T["carte"],"-","md",tc=T["texte"]); btns[key_m]=rm + rp=pygame.Rect(r.right-98,yy+44,90,56); self._btn(rp,T["carte"],"+","md",tc=T["texte"]); btns[key_p]=rp + pm_card("Compte ร  rebours (s)",cc.get("compte_a_rebours",3),"delay_m","delay_p",W//2-890,y) + pm_card("Copies impression", ci.get("copies_max",1), "copies_m","copies_p",W//2-440,y) + pm_card("Dรฉlai entre poses (s)",cc.get("delai_entre",1), "delayi_m","delayi_p",W//2+10,y) + # Imprimante + y2=y+158; r=pygame.Rect(W//2-890,y2,1780,80); self._card(r) + s=self._txt("Imprimante","xs",T["texte2"],False); self.scr.blit(s,(r.x+16,y2+10)) + imp=ci.get("imprimante","Dรฉtection automatique (premiรจre disponible)") + sv=self.F["xs"].render(str(imp),True,T["texte"]); self.scr.blit(sv,(r.x+16,y2+46)) + # Camรฉra + y3=y2+96; r2=pygame.Rect(W//2-890,y3,1780,80); self._card(r2) + s=self._txt("Camรฉra","xs",T["texte2"],False); self.scr.blit(s,(r2.x+16,y3+10)) + mode="DSLR connectรฉ (gphoto2)" if (self.cam and not self.sim) else "Simulation (pas de DSLR)" + col=T["prim"] if (self.cam and not self.sim) else (244,67,54) + sv=self.F["xs"].render(mode,True,col); self.scr.blit(sv,(r2.x+16,y3+46)) + rb=pygame.Rect(r2.right-230,y3+14,210,54); self._btn(rb,T["carte"],"Reconnecter","xs",tc=T["texte"]); btns["reconnect_cam"]=rb + + def _adm_compteur(self,btns,y): + T=self.T; cc=self.cfg.get("compteur",{}) + actif=cc.get("actif",False); prises=cc.get("photos_prises",self.compteur); limite=cc.get("limite",400) + # Toggle + self._tog_row(btns,"compteur_actif","Compteur actif",actif,W//2-440,y,880) + y+=84 + # Card : nombre de photos prises (font lg=96px, height~106px โ†’ card 240px) + card=pygame.Rect(W//2-440,y,880,240); self._card(card) + sv=self.F["lg"].render(str(prises),True,T["prim"]) + self.scr.blit(sv,(card.centerx-sv.get_width()//2,y+12)) + s=self._txt("photos prises","sm",T["texte2"],False) + self.scr.blit(s,(card.centerx-s.get_width()//2,y+130)) + s=self._txt(f"{max(0,limite-prises)} restantes / {limite}","xs",T["texte2"],False) + self.scr.blit(s,(card.centerx-s.get_width()//2,y+186)) + y+=260 + # Limite : label centrรฉ + -/valeur/+ + slbl=self._txt("Limite","xs",T["texte2"],False); self._c(slbl,y); y+=42 + rl=pygame.Rect(W//2-300,y,110,76); self._btn(rl,T["carte"],"-","lg",36,tc=T["texte"]); btns["lim_m"]=rl + sv2=self.F["md"].render(str(limite),True,T["prim"]); self.scr.blit(sv2,(W//2-sv2.get_width()//2,y+12)) + rr2=pygame.Rect(W//2+190,y,110,76); self._btn(rr2,T["carte"],"+","lg",36,tc=T["texte"]); btns["lim_p"]=rr2 + y+=100 + rrs=pygame.Rect(W//2-210,y,420,64); self._btn(rrs,(244,67,54),"Remettre a 0","sm"); btns["reset"]=rrs + + def _adm_evenement(self,btns,y): + T=self.T; ev=self.cfg.get("evenement",{}) + nom=ev.get("nom","โ€”"); theme=ev.get("theme","base") + sv=self._txt(nom,"lg",T["prim"]); self._c(sv,y); y+=sv.get_height()+16 + s=self._txt("Thรจme visuel","sm",T["texte2"],False); self._c(s,y); y+=46 + tl=[("base","Sombre",(26,26,46),(233,30,99),(255,255,255)), + ("photobooth","Photobooth",(235,242,255),(25,100,230),(15,30,80)), + ("sikapics","SikaPics",(251,231,232),(195,31,72),(36,4,57)), + ("copydev","CopyDev",(8,18,40),(41,182,246),(255,255,255))] + cw=320; gap=36; tx=(W-(4*cw+3*gap))//2 + for idx,(tk,tn,bg,prim,tex) in enumerate(tl): + x=tx+idx*(cw+gap); r=pygame.Rect(x,y,cw,220) + bord=prim if tk==theme else (80,80,100); bw=4 if tk==theme else 2 + self._card(r,bord=bord,fond=bg,bord_w=bw) + pygame.draw.circle(self.scr,prim,(r.centerx,y+82),38) + pygame.draw.circle(self.scr,tex,(r.centerx,y+82),16) + st=self.F["sm"].render(tn,True,tex); self.scr.blit(st,(r.centerx-st.get_width()//2,y+146)) + if tk==theme: sc=self.F["xs"].render("โœ“",True,prim); self.scr.blit(sc,(r.centerx-sc.get_width()//2,y+186)) + btns[("theme",tk)]=r + y+=240 + ip=_web_state.get("ip","?") + s=self._txt(f"Modifier le nom via mobile โ†’ http://{ip}/wizard","xs",T["texte2"],False); self._c(s,y) + + def _adm_systeme(self,btns,y): + T=self.T; now=time.time() + if now-self._sys_info_t>5: + info={} + try: info["ip"]=subprocess.check_output("hostname -I",shell=True).decode().strip() + except: info["ip"]="?" + try: + with open('/proc/meminfo') as f: + m={l.split(':')[0]:int(l.split(':')[1].strip().split()[0]) for l in f if ':' in l} + info["ram"]=f"{m.get('MemAvailable',0)//1024} Mo libres / {m.get('MemTotal',0)//1024} Mo" + except: info["ram"]="?" + try: + import shutil; tot,_,fr=shutil.disk_usage("/") + info["disque"]=f"{fr//1e9:.1f} Go libres / {tot//1e9:.1f} Go" + except: info["disque"]="?" + try: + with open('/sys/class/thermal/thermal_zone0/temp') as f: + info["temp"]=f"{int(f.read().strip())//1000}ยฐC" + except: info["temp"]="N/A" + info["camera"]="DSLR (gphoto2)" if (self.cam and not self.sim) else "Simulation" + self._sys_info=info; self._sys_info_t=now + rows=[("IP rรฉseau", self._sys_info.get("ip","?")), + ("RAM", self._sys_info.get("ram","?")), + ("Disque /", self._sys_info.get("disque","?")), + ("Tempรฉrature CPU", self._sys_info.get("temp","?")), + ("Camรฉra", self._sys_info.get("camera","?")), + ("Admin mobile", f"http://{_web_state.get('ip','?')}/")] + card=pygame.Rect(W//2-600,y,1200,len(rows)*60+20); self._card(card) + ry=y+16 + for label,val in rows: + sl=self.F["xs"].render(label,True,T["texte2"]); self.scr.blit(sl,(card.x+20,ry)) + sv=self.F["xs"].render(str(val),True,T["texte"]); self.scr.blit(sv,(card.x+300,ry)); ry+=60 + # QR admin (droite) + ip=_web_state.get("ip") + if ip and ip!="?" and HAS_QR and self._admin_qr is None: + try: + qimg=qrlib.make(f"http://{ip}/",border=2).convert("RGB").resize((200,200),Image.NEAREST) + self._admin_qr=pygame.image.fromstring(qimg.tobytes(),qimg.size,"RGB") + except: pass + if self._admin_qr: + qx=card.right+24; qy=y + pygame.draw.rect(self.scr,(255,255,255),pygame.Rect(qx-5,qy-5,210,210),border_radius=6) + self.scr.blit(self._admin_qr,(qx,qy)) + sq=self._txt("Scanner","xxs",T["texte2"],False); self.scr.blit(sq,(qx+100-sq.get_width()//2,qy+208)) + # Boutons systรจme + y2=card.bottom+20 + rb=pygame.Rect(W//2-360,y2,320,68); self._btn(rb,(255,152,0),"Redรฉmarrer","sm"); btns["reboot"]=rb + re=pygame.Rect(W//2+40,y2,320,68); self._btn(re,(244,67,54),"ร‰teindre","sm"); btns["shutdown"]=re + + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + # BOUCLE PRINCIPALE + # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + def run(self): + mbtn={}; gbtn={}; sbtn={}; ebtn={}; wbtn={}; abtn={}; fmtbtn={}; pbtn={} + last_click=0; running=True + + while running: + for ev in pygame.event.get(): + if ev.type==pygame.QUIT: running=False + if ev.type==pygame.KEYDOWN and ev.key==pygame.K_ESCAPE: running=False + + pos=None + if ev.type==pygame.MOUSEBUTTONDOWN and ev.button==1: pos=pygame.mouse.get_pos() + elif ev.type==pygame.FINGERDOWN: pos=(int(ev.x*W),int(ev.y*H)) + if not pos: continue + now=time.time() + if now-last_click<0.3: continue + last_click=now + e=self.etat + + # โ”€โ”€ Accueil โ”€โ”€ + if e==S_ACCUEIL: + az=pygame.Rect(W-80,H-80,66,66) + if az.collidepoint(pos): + self.pin_buf=""; self.pin_err=False; self.etat=S_PIN + else: + feat=self.cfg.get("fonctionnalites",{}) + s_ok=feat.get("photo_simple",True); m_ok=feat.get("multi_shot",True) + if s_ok and m_ok: self.etat=S_MODE + elif m_ok: self.mode="multi"; self.etat=S_FORMAT + else: self.mode="simple"; threading.Thread(target=self._sequence,daemon=True).start() + + # โ”€โ”€ Mode โ”€โ”€ + elif e==S_MODE: + if mbtn.get("simple",pygame.Rect(0,0,0,0)).collidepoint(pos): + self.mode="simple"; threading.Thread(target=self._sequence,daemon=True).start() + elif mbtn.get("multi",pygame.Rect(0,0,0,0)).collidepoint(pos): + self.mode="multi"; self.etat=S_FORMAT + elif mbtn.get("retour",pygame.Rect(0,0,0,0)).collidepoint(pos): + self.etat=S_ACCUEIL; self.t0=time.time() + + # โ”€โ”€ Format โ”€โ”€ + elif e==S_FORMAT: + if fmtbtn.get("retour",pygame.Rect(0,0,0,0)).collidepoint(pos): + self.etat=S_MODE + else: + for key,_,nb,__ in FORMATS: + if fmtbtn.get(key,pygame.Rect(0,0,0,0)).collidepoint(pos): + self.format=key + c=cfg_load(); c.setdefault("multi_shot",{})["format"]=key; cfg_save(c) + threading.Thread(target=self._sequence,daemon=True).start() + break + + # โ”€โ”€ Galerie โ”€โ”€ + elif e==S_GAL: + if gbtn.get("nouveau",pygame.Rect(0,0,0,0)).collidepoint(pos): self.etat=S_ACCUEIL; self.t0=time.time() + elif gbtn.get("imprimer",pygame.Rect(0,0,0,0)).collidepoint(pos): + if self.photos: self.photo_active=self.photos[0]; self.share_msg=""; self.etat=S_SHARE + elif gbtn.get("email",pygame.Rect(0,0,0,0)).collidepoint(pos): + if self.photos: self.photo_active=self.photos[0]; self.email_buf=""; self.email_msg=""; self.etat=S_EMAIL + + # โ”€โ”€ Partage โ”€โ”€ + elif e==S_SHARE: + if sbtn.get("terminer",pygame.Rect(0,0,0,0)).collidepoint(pos): self.etat=S_ACCUEIL; self.t0=time.time() + elif sbtn.get("nouveau",pygame.Rect(0,0,0,0)).collidepoint(pos): + self.mode="simple"; threading.Thread(target=self._sequence,daemon=True).start() + elif sbtn.get("imprimer",pygame.Rect(0,0,0,0)).collidepoint(pos): + ok=self._imprimer(self.photo_active) + self.share_msg="[OK] Impression lancee" if ok else "[ERR] Impression echouee" + elif sbtn.get("email",pygame.Rect(0,0,0,0)).collidepoint(pos): + self.email_buf=""; self.email_msg=""; self.etat=S_EMAIL + elif sbtn.get("qr",pygame.Rect(0,0,0,0)).collidepoint(pos): + self.share_msg=self.cfg.get("qr_code",{}).get("url_galerie","Pas d'URL") + + # โ”€โ”€ Email โ”€โ”€ + elif e==S_EMAIL: + if ebtn.get("annuler",pygame.Rect(0,0,0,0)).collidepoint(pos): + self.etat=S_SHARE if self.mode=="simple" else S_GAL + elif ebtn.get("envoyer",pygame.Rect(0,0,0,0)).collidepoint(pos): + if "@" in self.email_buf: + dest=self.email_buf; ch=self.photo_active; self.email_msg="Envoi..." + def _s(d=dest,c=ch): + ok=self._email_send(d,c) + self.email_msg="[OK] Email envoye !" if ok else "[ERR] Echec envoi" + threading.Thread(target=_s,daemon=True).start() + else: self.email_msg="[ERR] Adresse invalide" + else: + for ekey,r in ebtn.items(): + if not isinstance(ekey,tuple) or len(ekey)!=2: continue + t,v = ekey + if t=="k" and r.collidepoint(pos): + self.email_buf=self.email_buf[:-1] if v=="<--" else self.email_buf+v; break + + # โ”€โ”€ Wizard โ”€โ”€ + elif e==S_WIZ: + if wbtn.get("fermer",pygame.Rect(0,0,0,0)).collidepoint(pos): + self.etat=S_ACCUEIL; self.t0=time.time() + elif wbtn.get("next",pygame.Rect(0,0,0,0)).collidepoint(pos): + if self.wiz_step==1: + self.wiz_data["nom"]=self.wiz_kbbuf; self.wiz_step=2 + elif self.wiz_step==2: + self.wiz_data["session_code"]=self.wiz_code_buf; self.wiz_step=3 + elif self.wiz_step<7: self.wiz_step+=1 + elif wbtn.get("prev",pygame.Rect(0,0,0,0)).collidepoint(pos): + if self.wiz_step>1: self.wiz_step-=1 + elif wbtn.get("lancer",pygame.Rect(0,0,0,0)).collidepoint(pos): + self._wiz_appliquer(); self.etat=S_ACCUEIL; self.t0=time.time() + elif wbtn.get("moins",pygame.Rect(0,0,0,0)).collidepoint(pos): + self.wiz_data["nb_photos"]=max(1,self.wiz_data.get("nb_photos",4)-1) + elif wbtn.get("plus",pygame.Rect(0,0,0,0)).collidepoint(pos): + self.wiz_data["nb_photos"]=min(8,self.wiz_data.get("nb_photos",4)+1) + elif wbtn.get("code_gen",pygame.Rect(0,0,0,0)).collidepoint(pos): + import random + self.wiz_code_buf=f"{random.randint(100000000,999999999)}" + self.wiz_code_status=None + elif wbtn.get("code_test",pygame.Rect(0,0,0,0)).collidepoint(pos): + if len(self.wiz_code_buf)==9: + threading.Thread(target=self._test_session_code,args=(self.wiz_code_buf,),daemon=True).start() + else: + for key,r in wbtn.items(): + if not isinstance(key,tuple) or len(key)!=2: continue + if not r.collidepoint(pos): continue + t,v = key + if t=="k": + if v=="<--": self.wiz_kbbuf=self.wiz_kbbuf[:-1] + elif v=="Espace": self.wiz_kbbuf+=" " + else: self.wiz_kbbuf+=v + elif t=="code": + if v=="โŒซ": self.wiz_code_buf=self.wiz_code_buf[:-1]; self.wiz_code_status=None + elif v=="OK": + if len(self.wiz_code_buf)==9: + self.wiz_data["session_code"]=self.wiz_code_buf; self.wiz_step=3 + elif v.isdigit() and len(self.wiz_code_buf)<9: + self.wiz_code_buf+=v; self.wiz_code_status=None + elif t=="theme": + self.wiz_data["theme"]=v; self._apply_theme(v) + elif t=="mode": + m=self.wiz_data.setdefault("modes",{"photo_simple":True,"multi_shot":True}) + m[v]=not m.get(v,True) + elif t=="share": + s=self.wiz_data.setdefault("share",{"impression":True,"email":True,"qr_code":True,"galerie":True}) + s[v]=not s.get(v,True) + break + + # โ”€โ”€ PIN โ”€โ”€ + elif e==S_PIN: + if pbtn.get("annuler",pygame.Rect(0,0,0,0)).collidepoint(pos): + self.pin_buf=""; self.pin_err=False; self.etat=S_ACCUEIL + else: + for pkey,r in pbtn.items(): + if not isinstance(pkey,tuple) or len(pkey)!=2: continue + t,v = pkey + if t=="pin" and r.collidepoint(pos): + if v=="โŒซ": self.pin_buf=self.pin_buf[:-1] + elif v=="OK": + if self.pin_buf=="1234": + self.pin_buf=""; self.pin_err=False; self.etat=S_ADMIN + else: self.pin_err=True; self.pin_buf="" + elif len(self.pin_buf)<4: self.pin_buf+=v + break + + # โ”€โ”€ Admin โ”€โ”€ + elif e==S_ADMIN: + R=lambda k: abtn.get(k,pygame.Rect(0,0,0,0)) + if R("retour").collidepoint(pos): self.etat=S_ACCUEIL; self.t0=time.time() + elif R("wizard").collidepoint(pos): self._wiz_init(); self.etat=S_WIZ + elif R("reset").collidepoint(pos): self.compteur=0; self._sav_cpt() + elif R("delay_m").collidepoint(pos): self._admin_adj("camera","compte_a_rebours",-1,1,10) + elif R("delay_p").collidepoint(pos): self._admin_adj("camera","compte_a_rebours",+1,1,10) + elif R("delayi_m").collidepoint(pos): self._admin_adj("camera","delai_entre",-1,0,5) + elif R("delayi_p").collidepoint(pos): self._admin_adj("camera","delai_entre",+1,0,5) + elif R("copies_m").collidepoint(pos): self._admin_adj("impression","copies_max",-1,1,10) + elif R("copies_p").collidepoint(pos): self._admin_adj("impression","copies_max",+1,1,10) + elif R("lim_m").collidepoint(pos): self._admin_adj("compteur","limite",-50,50,9999) + elif R("lim_p").collidepoint(pos): self._admin_adj("compteur","limite",+50,50,9999) + elif R("reconnect_cam").collidepoint(pos): threading.Thread(target=self._reconnect_cam,daemon=True).start() + elif R("reboot").collidepoint(pos): subprocess.Popen(["sudo","reboot"]) + elif R("shutdown").collidepoint(pos): subprocess.Popen(["sudo","shutdown","-h","now"]) + else: + for akey,r in abtn.items(): + if not isinstance(akey,tuple) or len(akey)!=2: continue + if not r.collidepoint(pos): continue + tp,key = akey + if tp=="t": + f=self.cfg.setdefault("fonctionnalites",{}) + if key=="compteur_actif": self.cfg.setdefault("compteur",{})["actif"]=not self.cfg.get("compteur",{}).get("actif",False) + elif key=="compteur_feat": f[key]=not f.get(key,True) + else: f[key]=not f.get(key,True) + cfg_save(self.cfg) + elif tp=="tab": self.admin_tab=key + elif tp=="theme": + self.cfg.setdefault("evenement",{})["theme"]=key + cfg_save(self.cfg); self._apply_theme(key) + break + + # โ”€โ”€ Dessin โ”€โ”€ + e=self.etat + if e==S_ACCUEIL: self.draw_accueil() + elif e==S_MODE: mbtn=self.draw_mode() + elif e==S_PREV: self.draw_preview() + elif e==S_COMPTE: self.draw_compte() + elif e==S_CAP: self.draw_capture() + elif e==S_GAL: gbtn=self.draw_galerie() + elif e==S_SHARE: sbtn=self.draw_partage() + elif e==S_EMAIL: ebtn=self.draw_email() + elif e==S_FORMAT: fmtbtn=self.draw_format() + elif e==S_WIZ: wbtn=self.draw_wizard() + elif e==S_PIN: pbtn=self.draw_pin() + elif e==S_ADMIN: abtn=self.draw_admin() + + # Sync config (web mobile Flask ET FastAPI backend) โ€” toutes les 2s + _now2 = time.time() + if _now2 - self._cfg_check > 2: + self._cfg_check = _now2 + try: + _mt = CONFIG_PATH.stat().st_mtime + if _mt != self._cfg_mtime or _web_state["reload_flag"].is_set(): + self._cfg_mtime = _mt + _web_state["reload_flag"].clear() + self.cfg = cfg_load() + self._apply_theme(self.cfg.get("evenement",{}).get("theme","base")) + self.qr_surf = self._make_qr() + self.compteur= self.cfg.get("compteur",{}).get("photos_prises",0) + self.cpt_max = self.cfg.get("compteur",{}).get("limite",400) + self._admin_qr = None # rรฉgรฉnรฉrer le QR si IP a changรฉ + print("Config rechargรฉe depuis le disque") + # Gรฉnรฉration lazy du QR si Flask vient de dรฉmarrer + if self.qr_surf is None: + self.qr_surf = self._make_qr() + except Exception as _e: + print(f"sync cfg: {_e}") + + pygame.display.flip() + self.clock.tick(30) + + self.prev_actif=False + try: self.cam.exit() + except: pass + pygame.quit() + + # โ”€โ”€ Admin helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _admin_adj(self, section, key, delta, mini, maxi): + c=cfg_load(); v=c.setdefault(section,{}).get(key,(mini+maxi)//2) + c[section][key]=max(mini,min(maxi,v+delta)); cfg_save(c); self.cfg=c + + def _reconnect_cam(self): + if self.sim: return + self.prev_actif=False; time.sleep(0.5) + try: + if self.cam: self.cam.exit() + except: pass + self.cam=None + for i in range(3): + try: self.cam=gp.Camera(); self.cam.init(); self.sim=False; self.prev_actif=True; print("Camera reconnectรฉe"); return + except: self.cam=None; time.sleep(1) + self.sim=True; print("Reconnexion รฉchouรฉe โ†’ simulation") + + # โ”€โ”€ Wizard : init & apply โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + def _wiz_init(self): + import random + ev=self.cfg.get("evenement",{}) + feat=self.cfg.get("fonctionnalites",{}) + self.wiz_step=1 + self.wiz_kbbuf=ev.get("nom","") + # Code session : rรฉutiliser l'existant ou en gรฉnรฉrer un nouveau + existing_code = self.cfg.get("booth",{}).get("session_code","") + self.wiz_code_buf = existing_code if existing_code else f"{random.randint(100000000,999999999)}" + self.wiz_code_status = None + self.wiz_data={ + "nom": ev.get("nom",""), + "theme": ev.get("theme","base"), + "modes": {"photo_simple":feat.get("photo_simple",True),"multi_shot":feat.get("multi_shot",True)}, + "nb_photos": self.cfg.get("multi_shot",{}).get("nombre_photos",4), + "share": {"impression":feat.get("impression",True),"email":feat.get("email",True), + "qr_code":feat.get("qr_code",True),"galerie":feat.get("galerie",True)}, + "session_code": self.wiz_code_buf, + } + + def _test_session_code(self, code): + """Teste si le code session est accessible sur le serveur (thread bg).""" + import urllib.request, random + self.wiz_code_status = "test..." + try: + url = f"https://photobooth.copydev.fr/s/{code}" + urllib.request.urlopen(url, timeout=6) + self.wiz_code_status = "ok" + except Exception as ex: + print(f"Test session code erreur: {ex}") + self.wiz_code_status = "erreur" + + def _wiz_appliquer(self): + c=cfg_load() + nom = self.wiz_data.get("nom","") or "photobooth" + c.setdefault("evenement",{})["nom"] = nom + c["evenement"]["theme"] = self.wiz_data.get("theme","base") + c.setdefault("multi_shot",{})["nombre_photos"] = self.wiz_data.get("nb_photos",4) + feat=c.setdefault("fonctionnalites",{}) + feat.update(self.wiz_data.get("modes",{})) + feat.update(self.wiz_data.get("share",{})) + # โ”€โ”€ Code session (saisi/gรฉnรฉrรฉ dans le wizard step 2) โ”€โ”€ + code = self.wiz_data.get("session_code","") or self.wiz_code_buf + if not code: import random; code=f"{random.randint(100000000,999999999)}" + b = c.setdefault("booth",{}) + b["url"] = "https://photobooth.copydev.fr" + b["session_code"] = code + b["actif"] = True + cfg_save(c) + self.cfg=c + self._apply_theme(self.wiz_data.get("theme","base")) + self.cpt_max=c.get("compteur",{}).get("limite",400) + # QR pointe vers la galerie de la session + sess_url = f"https://photobooth.copydev.fr/s/{code}" + try: + img = qrlib.make(sess_url, border=2).convert("RGB").resize((200,200),Image.NEAREST) + self.qr_surf = pygame.image.fromstring(img.tobytes(),img.size,"RGB") + except: + self.qr_surf = self._make_qr() + print(f"Session galerie: {sess_url} (code: {code})") + + +if __name__=="__main__": + LOG = Path("/tmp/photobooth_crash.log") + try: + PB().run() + except Exception as e: + msg = traceback.format_exc() + print(msg, flush=True) + LOG.write_text(f"{datetime.now()}\n{msg}") + time.sleep(5)