From 0b612e1302f8d962be8923b3bd3ef8060f037509 Mon Sep 17 00:00:00 2001 From: jbperrin Date: Fri, 17 Jul 2026 17:52:52 +0200 Subject: [PATCH] feat: impression depuis galerie publique (quota session, modal copies) --- backend/api/print_api.py | 88 ++++++++- frontend/templates/public/gallery.html | 248 ++++++++++++++++++++++++- 2 files changed, 326 insertions(+), 10 deletions(-) diff --git a/backend/api/print_api.py b/backend/api/print_api.py index c2af3f3..f4a4741 100644 --- a/backend/api/print_api.py +++ b/backend/api/print_api.py @@ -142,12 +142,96 @@ async def get_user_print_config(request: Request): """Retourne la config impression galerie utilisateur.""" cfg = request.app.state.config.print return { - "user_print_enabled": cfg.user_print_enabled, - "user_print_quota": cfg.user_print_quota, + "user_print_enabled": cfg.user_print_enabled, + "user_print_quota": cfg.user_print_quota, "user_print_max_copies": cfg.user_print_max_copies, } +@router.get("/print/user-print-status") +async def get_user_print_status(request: Request): + """Statut d'impression de la session courante (quota restant, config active).""" + cfg = request.app.state.config.print + count = request.session.get("gallery_print_count", 0) + remaining = (cfg.user_print_quota - count) if cfg.user_print_quota > 0 else -1 + return { + "user_print_enabled": cfg.user_print_enabled, + "user_print_quota": cfg.user_print_quota, + "user_print_max_copies": cfg.user_print_max_copies, + "session_print_count": count, + "remaining_quota": remaining, # -1 = illimité + } + + +@router.post("/print/request-gallery") +async def gallery_print_request(request: Request, body: dict = Body(...)): + """Demande d'impression depuis la galerie publique. + Vérifie que l'impression est activée et que le quota session n'est pas dépassé. + """ + from pathlib import Path as _Path + + cfg_print = request.app.state.config.print + + # ── Activation ──────────────────────────────────────────────────────────── + if not cfg_print.user_print_enabled: + return JSONResponse({"error": "L'impression est désactivée"}, status_code=403) + + photo_id = str(body.get("photo_id", "")).strip() + if not photo_id: + return JSONResponse({"error": "photo_id requis"}, status_code=400) + + copies = max(1, min(cfg_print.user_print_max_copies, int(body.get("copies", 1)))) + + # ── Quota session ───────────────────────────────────────────────────────── + if cfg_print.user_print_quota > 0: + count = request.session.get("gallery_print_count", 0) + if count >= cfg_print.user_print_quota: + return JSONResponse({ + "error": f"Quota atteint ({cfg_print.user_print_quota} impression(s) maximum par session)", + "quota_exceeded": True, + }, status_code=429) + + # ── Résolution du fichier ───────────────────────────────────────────────── + media_dir = _Path(request.app.state.config.photobooth.media_dir) + candidates = sorted(media_dir.glob(f"{photo_id}*")) + if not candidates: + return JSONResponse({"error": "Photo introuvable"}, status_code=404) + filepath = str(candidates[0]) + + # ── Enqueue ─────────────────────────────────────────────────────────────── + pb = request.app.state.photobooth_service + printer_svc = request.app.state.printer_service + ws = request.app.state.ws_manager + led = request.app.state.led_service + + thumb_url = pb.thumbnail_url(photo_id) + entry = await printer_svc.add_request(filepath, thumb_url, copies) + await ws.broadcast({"type": "print_request", "entry": entry}) + + # ── Incrémenter compteur session ────────────────────────────────────────── + new_count = request.session.get("gallery_print_count", 0) + 1 + request.session["gallery_print_count"] = new_count + remaining = (cfg_print.user_print_quota - new_count) if cfg_print.user_print_quota > 0 else -1 + + # ── Stats événement ─────────────────────────────────────────────────────── + event_svc = getattr(request.app.state, "event_service", None) + if event_svc: + slug = request.app.state.config.event.slug + asyncio.create_task(event_svc.increment(slug, "print_requests")) + + # ── LED si mode direct ──────────────────────────────────────────────────── + if request.app.state.config.print.mode == "direct": + led.play("printing") + + logger.info("Impression galerie: photo_id=%s copies=%d session_count=%d", photo_id, copies, new_count) + return { + "ok": True, + "id": entry["id"], + "mode": request.app.state.config.print.mode, + "remaining_quota": remaining, + } + + # ── Gestion avancée des imprimantes CUPS ────────────────────────────────────── @router.post("/print/printers/{printer_name}/enable") diff --git a/frontend/templates/public/gallery.html b/frontend/templates/public/gallery.html index b4ffe2f..7169e45 100644 --- a/frontend/templates/public/gallery.html +++ b/frontend/templates/public/gallery.html @@ -11,6 +11,59 @@ pointer-events: none; } .photo-card { position: relative; } + +/* ── Bouton imprimer sur la card ── */ +.photo-card-actions { display: flex; gap: .35rem; flex-wrap: wrap; } + +/* ── Bandeau quota ── */ +#quota-bar { + display: none; + align-items: center; + gap: .6rem; + background: rgba(25,108,176,.12); + border: 1px solid rgba(25,108,176,.3); + border-radius: 8px; + padding: .45rem .85rem; + font-size: .88rem; + margin-bottom: .75rem; +} +#quota-bar.quota-warn { + background: rgba(224,123,0,.12); + border-color: rgba(224,123,0,.35); +} +#quota-bar.quota-empty { + background: rgba(192,0,0,.1); + border-color: rgba(192,0,0,.3); +} + +/* ── Modal copies ── */ +.modal-overlay { + display: none; + position: fixed; inset: 0; + background: rgba(0,0,0,.65); + z-index: 2000; + align-items: center; + justify-content: center; +} +.modal-overlay.open { display: flex; } +.modal-box { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 14px; + padding: 1.5rem; + max-width: 340px; + width: 90%; + display: flex; + flex-direction: column; + gap: 1rem; +} +.modal-title { font-size: 1.05rem; font-weight: 700; } +.modal-preview { width: 100%; border-radius: 8px; object-fit: cover; max-height: 180px; } +.copies-row { display: flex; align-items: center; gap: .75rem; } +.copies-btn { background: var(--bg-muted); border: 1px solid var(--border); color: var(--text); + width: 2rem; height: 2rem; border-radius: 6px; cursor: pointer; font-size: 1.1rem; + display: flex; align-items: center; justify-content: center; } +.copies-val { font-size: 1.2rem; font-weight: 700; min-width: 1.5rem; text-align: center; } {% endblock %} @@ -27,6 +80,12 @@ Retrouvez vos photos ci-dessous. Scannez le QR code depuis la galerie du photomaton pour télécharger votre photo directement sur votre téléphone.

+ +
+ 🖨 + +
+
@@ -45,16 +104,83 @@ Photo
Connectez-vous au WiFi Photomaton pour télécharger
+ + + {% endblock %} {% block scripts %} {% endblock %}