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 @@
Connectez-vous au WiFi Photomaton pour télécharger
+
+
+
+
+
🖨 Demande d'impression
+
![]()
+
+ Nombre de copies
+
+ 1
+
+
+
+
+
+
+
+
+
{% endblock %}
{% block scripts %}
{% endblock %}