Galerie live booth.copydev.fr + icones agrandies + pellicule

- booth/: serveur Node.js galerie photo live avec auth par code 4 chiffres
- Backend: envoi auto des photos vers booth, endpoint QR dynamique
- Frontend: code + QR booth sur ecran accueil, icone pellicule pour multi-shot
- Icones mode agrandies (5rem)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 17:27:05 +02:00
parent e583126f85
commit f0584e8fdf
8 changed files with 835 additions and 4 deletions

View File

@@ -1,7 +1,10 @@
import ftplib import ftplib
import logging import logging
import shutil import shutil
import threading
from pathlib import Path from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import URLError
from backend.config import charger_config, mettre_a_jour_config from backend.config import charger_config, mettre_a_jour_config
@@ -26,6 +29,15 @@ def distribuer_photo(chemin_photo: Path, imprimee: bool = False):
if dest.get("ftp", False): if dest.get("ftp", False):
envoyer_ftp(chemin_photo, dest) envoyer_ftp(chemin_photo, dest)
# Galerie live (booth)
booth = config.get("booth", {})
if booth.get("actif", False):
threading.Thread(
target=envoyer_booth,
args=(chemin_photo, booth),
daemon=True,
).start()
# Incrementer le compteur # Incrementer le compteur
compteur = config.get("compteur", {}) compteur = config.get("compteur", {})
if compteur.get("actif", False): if compteur.get("actif", False):
@@ -83,6 +95,73 @@ def envoyer_ftp(chemin_photo: Path, config_dest: dict):
return False return False
def envoyer_booth(chemin_photo: Path, config_booth: dict):
"""Envoie une photo vers la galerie live booth."""
url = config_booth.get("url", "").rstrip("/")
api_key = config_booth.get("api_key", "")
event_id = config_booth.get("event_id", "default")
if not url:
log.warning("Booth non configure (URL manquante)")
return False
try:
import mimetypes
boundary = "----BoothUpload"
filename = chemin_photo.name
with open(chemin_photo, "rb") as f:
file_data = f.read()
body = (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="photo"; filename="{filename}"\r\n'
f"Content-Type: image/jpeg\r\n\r\n"
).encode() + file_data + f"\r\n--{boundary}--\r\n".encode()
req = Request(
f"{url}/api/{event_id}/upload",
data=body,
method="POST",
)
req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")
req.add_header("X-Api-Key", api_key)
with urlopen(req, timeout=10) as resp:
if resp.status == 200:
log.info(f"Photo envoyee au booth : {filename}")
return True
else:
log.error(f"Booth erreur HTTP {resp.status}")
return False
except (URLError, OSError) as e:
log.error(f"Erreur envoi booth : {e}")
return False
def recuperer_booth_password() -> dict:
"""Recupere le mot de passe et l'info de la session booth."""
config = charger_config()
booth = config.get("booth", {})
url = booth.get("url", "").rstrip("/")
api_key = booth.get("api_key", "")
event_id = booth.get("event_id", "default")
if not url:
return {"password": None}
try:
req = Request(f"{url}/api/{event_id}/info")
req.add_header("X-Api-Key", api_key)
with urlopen(req, timeout=5) as resp:
import json
data = json.loads(resp.read())
return data
except (URLError, OSError) as e:
log.error(f"Erreur recuperation booth info : {e}")
return {"password": None}
def detecter_usb() -> list[str]: def detecter_usb() -> list[str]:
"""Detecte les cles USB montees.""" """Detecte les cles USB montees."""
chemins_possibles = [Path("/media"), Path("/mnt")] chemins_possibles = [Path("/media"), Path("/mnt")]

View File

@@ -17,7 +17,7 @@ from backend.camera import camera
from backend.gallery import lister_photos, compter_photos, supprimer_photo, vider_galerie 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.effects import appliquer_filtre, appliquer_overlay, chroma_key, lister_overlays, lister_fonds, FILTRES
from backend.collage import creer_strip, creer_collage, creer_impression_strip from backend.collage import creer_strip, creer_collage, creer_impression_strip
from backend.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur from backend.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur, recuperer_booth_password
from backend.printer import lister_imprimantes, imprimer from backend.printer import lister_imprimantes, imprimer
from backend.mailer import envoyer_photo from backend.mailer import envoyer_photo
from backend.qrcode_gen import generer_qr, qr_galerie from backend.qrcode_gen import generer_qr, qr_galerie
@@ -243,6 +243,13 @@ async def api_imprimer(donnees: dict):
return {"succes": ok} return {"succes": ok}
# --- API Booth (galerie live) ---
@app.get("/api/booth/info")
async def api_booth_info():
return recuperer_booth_password()
# --- API Email --- # --- API Email ---
@app.post("/api/email") @app.post("/api/email")
@@ -262,6 +269,21 @@ async def api_email(donnees: dict):
# --- API QR Code --- # --- API QR Code ---
@app.get("/api/qr")
async def api_qr(url: str = ""):
"""Genere un QR code pour une URL arbitraire."""
if not url:
return JSONResponse({"erreur": "URL requise"}, status_code=400)
import qrcode
import io
qr = qrcode.make(url, box_size=6, border=2)
buf = io.BytesIO()
qr.save(buf, format="PNG")
buf.seek(0)
from fastapi.responses import StreamingResponse
return StreamingResponse(buf, media_type="image/png")
@app.get("/api/qr/galerie") @app.get("/api/qr/galerie")
async def api_qr_galerie(): async def api_qr_galerie():
chemin = qr_galerie() chemin = qr_galerie()

16
booth/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "booth-gallery",
"version": "1.0.0",
"description": "Galerie photo live pour photobooth",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"dependencies": {
"express": "^4.21.0",
"ws": "^8.18.0",
"multer": "^1.4.5-lts.1",
"sharp": "^0.33.5"
}
}

432
booth/public/index.html Normal file
View File

@@ -0,0 +1,432 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Galerie Photo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #0a0a0f;
--card: #14141f;
--accent: #e91e63;
--text: #ffffff;
--text2: #8888aa;
--radius: 16px;
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
/* --- Ecran de login --- */
#login-screen {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 2rem;
}
#login-screen.hidden { display: none; }
#gallery-screen.hidden { display: none; }
.login-box {
background: var(--card);
border-radius: var(--radius);
padding: 3rem;
max-width: 400px;
width: 100%;
text-align: center;
}
.login-box h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.login-box p {
color: var(--text2);
margin-bottom: 2rem;
}
.login-box .icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.code-input {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 1.5rem;
}
.code-input input {
width: 60px;
height: 70px;
text-align: center;
font-size: 2rem;
font-weight: bold;
border: 2px solid #333;
border-radius: 12px;
background: var(--bg);
color: var(--text);
outline: none;
-webkit-appearance: none;
}
.code-input input:focus {
border-color: var(--accent);
}
.login-error {
color: #f44336;
font-size: 0.9rem;
margin-top: 0.5rem;
display: none;
}
.login-error.visible { display: block; }
/* --- Galerie --- */
#gallery-screen {
padding: 1rem;
}
.gallery-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
margin-bottom: 1rem;
}
.gallery-header h1 {
font-size: 1.5rem;
}
.photo-count {
color: var(--text2);
font-size: 0.9rem;
}
.live-badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(233, 30, 99, 0.2);
color: var(--accent);
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
}
.live-dot {
width: 8px;
height: 8px;
background: var(--accent);
border-radius: 50%;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.photo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 12px;
padding: 0 1rem 2rem;
}
.photo-card {
position: relative;
border-radius: var(--radius);
overflow: hidden;
cursor: pointer;
aspect-ratio: 4/3;
background: var(--card);
animation: fadeIn 0.4s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
.photo-card img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s;
}
.photo-card:active img {
transform: scale(1.05);
}
.photo-time {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0,0,0,0.6);
color: white;
padding: 2px 8px;
border-radius: 8px;
font-size: 0.75rem;
}
/* --- Lightbox --- */
.lightbox {
display: none;
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.95);
z-index: 100;
align-items: center;
justify-content: center;
flex-direction: column;
}
.lightbox.active { display: flex; }
.lightbox img {
max-width: 95%;
max-height: 85vh;
border-radius: 8px;
object-fit: contain;
}
.lightbox-close {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
color: white;
font-size: 2.5rem;
cursor: pointer;
padding: 0.5rem;
}
.lightbox-download {
margin-top: 1rem;
background: var(--accent);
color: white;
border: none;
padding: 12px 24px;
border-radius: 12px;
font-size: 1rem;
cursor: pointer;
}
/* --- Responsive --- */
@media (max-width: 600px) {
.photo-grid {
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.login-box { padding: 2rem; }
.code-input input { width: 50px; height: 60px; font-size: 1.5rem; }
}
/* --- Empty state --- */
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: var(--text2);
}
.empty-state .icon { font-size: 4rem; margin-bottom: 1rem; }
</style>
</head>
<body>
<!-- Ecran de login -->
<div id="login-screen">
<div class="login-box">
<div class="icon">&#128247;</div>
<h1>Galerie Photo</h1>
<p>Saisissez le code affiche sur la borne</p>
<div class="code-input">
<input type="tel" maxlength="1" data-index="0" inputmode="numeric" autocomplete="off">
<input type="tel" maxlength="1" data-index="1" inputmode="numeric" autocomplete="off">
<input type="tel" maxlength="1" data-index="2" inputmode="numeric" autocomplete="off">
<input type="tel" maxlength="1" data-index="3" inputmode="numeric" autocomplete="off">
</div>
<div id="login-error" class="login-error">Code incorrect</div>
</div>
</div>
<!-- Ecran galerie -->
<div id="gallery-screen" class="hidden">
<div class="gallery-header">
<div>
<h1>Galerie Photo</h1>
<span class="photo-count" id="photo-count">0 photos</span>
</div>
<span class="live-badge"><span class="live-dot"></span> LIVE</span>
</div>
<div class="photo-grid" id="photo-grid"></div>
<div class="empty-state" id="empty-state">
<div class="icon">&#128248;</div>
<p>Les photos apparaitront ici en temps reel</p>
</div>
</div>
<!-- Lightbox -->
<div class="lightbox" id="lightbox">
<button class="lightbox-close" onclick="closeLightbox()">&times;</button>
<img id="lightbox-img" src="">
<button class="lightbox-download" id="lightbox-dl" onclick="downloadPhoto()">Telecharger</button>
</div>
<script>
const eventId = window.location.pathname.replace(/^\//, '') || 'default';
let token = null;
let ws = null;
let photos = [];
// --- Login ---
const codeInputs = document.querySelectorAll('.code-input input');
codeInputs.forEach((input, i) => {
input.addEventListener('input', (e) => {
const val = e.target.value;
if (val && i < 3) codeInputs[i + 1].focus();
if (val && i === 3) tryLogin();
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && !e.target.value && i > 0) {
codeInputs[i - 1].focus();
}
});
});
codeInputs[0].focus();
async function tryLogin() {
const code = Array.from(codeInputs).map(i => i.value).join('');
if (code.length !== 4) return;
try {
const res = await fetch(`/api/${eventId}/auth`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: code }),
});
if (res.ok) {
const data = await res.json();
token = data.token;
document.getElementById('login-error').classList.remove('visible');
showGallery();
} else {
document.getElementById('login-error').classList.add('visible');
codeInputs.forEach(i => { i.value = ''; });
codeInputs[0].focus();
}
} catch (err) {
console.error(err);
}
}
// --- Galerie ---
async function showGallery() {
document.getElementById('login-screen').classList.add('hidden');
document.getElementById('gallery-screen').classList.remove('hidden');
// Charger les photos existantes
const res = await fetch(`/api/${eventId}/photos`, {
headers: { 'X-Token': token },
});
const data = await res.json();
photos = data.photos || [];
renderPhotos();
// Connexion WebSocket pour les nouvelles photos
connectWS();
}
function renderPhotos() {
const grid = document.getElementById('photo-grid');
const empty = document.getElementById('empty-state');
const count = document.getElementById('photo-count');
count.textContent = `${photos.length} photo${photos.length > 1 ? 's' : ''}`;
if (photos.length === 0) {
empty.style.display = 'block';
return;
}
empty.style.display = 'none';
// Afficher les photos les plus recentes en premier
const sorted = [...photos].reverse();
grid.innerHTML = sorted.map(p => `
<div class="photo-card" onclick="openLightbox('${p.name}')">
<img src="/thumbs/${eventId}/${p.name}?token=${token}" loading="lazy" alt="">
<span class="photo-time">${formatTime(p.timestamp)}</span>
</div>
`).join('');
}
function formatTime(ts) {
const d = new Date(ts);
return d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
}
// --- WebSocket live ---
function connectWS() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${proto}//${location.host}?event=${eventId}&token=${token}`);
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'new_photo') {
photos.push(msg.photo);
renderPhotos();
}
};
ws.onclose = () => {
setTimeout(connectWS, 3000);
};
}
// --- Lightbox ---
let currentPhoto = null;
function openLightbox(name) {
currentPhoto = name;
document.getElementById('lightbox-img').src = `/photos/${eventId}/${name}?token=${token}`;
document.getElementById('lightbox').classList.add('active');
}
function closeLightbox() {
document.getElementById('lightbox').classList.remove('active');
}
document.getElementById('lightbox').addEventListener('click', (e) => {
if (e.target === e.currentTarget) closeLightbox();
});
function downloadPhoto() {
if (!currentPhoto) return;
const a = document.createElement('a');
a.href = `/photos/${eventId}/${currentPhoto}?token=${token}`;
a.download = currentPhoto;
a.click();
}
</script>
</body>
</html>

224
booth/server.js Normal file
View File

@@ -0,0 +1,224 @@
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const multer = require('multer');
const sharp = require('sharp');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
// --- Configuration ---
const PORT = process.env.PORT || 3000;
const PHOTOS_DIR = path.join(__dirname, 'data', 'photos');
const THUMBS_DIR = path.join(__dirname, 'data', 'thumbs');
// Creer les dossiers
fs.mkdirSync(PHOTOS_DIR, { recursive: true });
fs.mkdirSync(THUMBS_DIR, { recursive: true });
// --- Sessions evenement ---
// { eventId: { password, photos: [{name, timestamp}], createdAt } }
const sessions = new Map();
function genererPassword() {
return crypto.randomInt(1000, 9999).toString();
}
function getSession(eventId) {
if (!sessions.has(eventId)) {
sessions.set(eventId, {
password: genererPassword(),
photos: [],
createdAt: Date.now(),
});
// Creer le dossier
fs.mkdirSync(path.join(PHOTOS_DIR, eventId), { recursive: true });
fs.mkdirSync(path.join(THUMBS_DIR, eventId), { recursive: true });
}
return sessions.get(eventId);
}
// --- Upload photos depuis la borne ---
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 20 * 1024 * 1024 }, // 20 Mo max
});
// API Key pour l'upload (simple)
const API_KEY = process.env.API_KEY || 'booth-secret-key';
function verifierApiKey(req, res, next) {
const key = req.headers['x-api-key'] || req.query.key;
if (key !== API_KEY) {
return res.status(401).json({ error: 'Cle API invalide' });
}
next();
}
// --- Routes API ---
// Upload photo depuis la borne
app.post('/api/:eventId/upload', verifierApiKey, upload.single('photo'), async (req, res) => {
try {
const { eventId } = req.params;
const session = getSession(eventId);
const timestamp = Date.now();
const filename = `${timestamp}.jpg`;
// Sauvegarder la photo originale
const photoPath = path.join(PHOTOS_DIR, eventId, filename);
await sharp(req.file.buffer)
.jpeg({ quality: 90 })
.toFile(photoPath);
// Creer la miniature
const thumbPath = path.join(THUMBS_DIR, eventId, filename);
await sharp(req.file.buffer)
.resize(400, 400, { fit: 'cover' })
.jpeg({ quality: 75 })
.toFile(thumbPath);
// Ajouter a la session
const photoInfo = { name: filename, timestamp };
session.photos.push(photoInfo);
// Notifier les clients WebSocket
broadcast(eventId, {
type: 'new_photo',
photo: photoInfo,
});
res.json({ ok: true, filename });
} catch (err) {
console.error('Erreur upload:', err);
res.status(500).json({ error: 'Erreur upload' });
}
});
// Info session (mot de passe pour la borne)
app.get('/api/:eventId/info', verifierApiKey, (req, res) => {
const session = getSession(req.params.eventId);
res.json({
password: session.password,
photoCount: session.photos.length,
});
});
// Regenerer le mot de passe
app.post('/api/:eventId/reset-password', verifierApiKey, (req, res) => {
const session = getSession(req.params.eventId);
session.password = genererPassword();
res.json({ password: session.password });
});
// --- Routes publiques (invites) ---
// Verifier le mot de passe
app.post('/api/:eventId/auth', express.json(), (req, res) => {
const session = sessions.get(req.params.eventId);
if (!session) {
return res.status(404).json({ error: 'Evenement introuvable' });
}
if (req.body.password !== session.password) {
return res.status(403).json({ error: 'Mot de passe incorrect' });
}
// Generer un token simple
const token = crypto.randomBytes(16).toString('hex');
session.token = session.token || new Set();
session.token.add(token);
res.json({ ok: true, token, photoCount: session.photos.length });
});
// Middleware pour verifier le token invite
function verifierToken(req, res, next) {
const token = req.headers['x-token'] || req.query.token;
const session = sessions.get(req.params.eventId);
if (!session || !session.token || !session.token.has(token)) {
return res.status(403).json({ error: 'Acces refuse' });
}
req.session = session;
next();
}
// Liste des photos
app.get('/api/:eventId/photos', verifierToken, (req, res) => {
res.json({ photos: req.session.photos });
});
// Servir les photos
app.get('/photos/:eventId/:filename', (req, res) => {
const token = req.query.token;
const session = sessions.get(req.params.eventId);
if (!session || !session.token || !session.token.has(token)) {
return res.status(403).send('Acces refuse');
}
const filePath = path.join(PHOTOS_DIR, req.params.eventId, req.params.filename);
if (!fs.existsSync(filePath)) return res.status(404).send('Not found');
res.sendFile(filePath);
});
// Servir les miniatures
app.get('/thumbs/:eventId/:filename', (req, res) => {
const token = req.query.token;
const session = sessions.get(req.params.eventId);
if (!session || !session.token || !session.token.has(token)) {
return res.status(403).send('Acces refuse');
}
const filePath = path.join(THUMBS_DIR, req.params.eventId, req.params.filename);
if (!fs.existsSync(filePath)) return res.status(404).send('Not found');
res.sendFile(filePath);
});
// --- WebSocket ---
wss.on('connection', (ws, req) => {
const url = new URL(req.url, `http://localhost:${PORT}`);
const eventId = url.searchParams.get('event');
const token = url.searchParams.get('token');
const session = sessions.get(eventId);
if (!session || !session.token || !session.token.has(token)) {
ws.close(4001, 'Acces refuse');
return;
}
ws.eventId = eventId;
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
ws.on('error', () => {});
});
function broadcast(eventId, data) {
const msg = JSON.stringify(data);
wss.clients.forEach(client => {
if (client.eventId === eventId && client.readyState === WebSocket.OPEN) {
client.send(msg);
}
});
}
// Ping pour garder les connexions
setInterval(() => {
wss.clients.forEach(ws => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
// --- Frontend statique ---
app.use(express.static(path.join(__dirname, 'public')));
// SPA fallback
app.get('/:eventId', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// --- Demarrage ---
server.listen(PORT, () => {
console.log(`Booth gallery running on port ${PORT}`);
});

View File

@@ -249,6 +249,36 @@ html, body {
font-size: 3rem; font-size: 3rem;
} }
.btn-mode .mode-icone-large {
font-size: 5rem;
}
/* === Booth info (code + QR sur accueil) === */
.booth-info {
position: absolute;
bottom: 1rem;
left: 1rem;
display: flex;
align-items: center;
gap: 0.8rem;
background: rgba(0,0,0,0.5);
padding: 0.5rem 1rem;
border-radius: 12px;
}
.booth-qr {
width: 64px;
height: 64px;
border-radius: 6px;
}
.booth-code {
font-size: 1.4rem;
font-weight: bold;
color: rgba(255,255,255,0.7);
letter-spacing: 4px;
}
/* === Capture / Preview live === */ /* === Capture / Preview live === */
.preview-live { .preview-live {
width: 100%; width: 100%;

View File

@@ -24,6 +24,11 @@
<div id="compteur-accueil" class="compteur-accueil cache"> <div id="compteur-accueil" class="compteur-accueil cache">
<span id="compteur-restant">400</span> / <span id="compteur-limite">400</span> <span id="compteur-restant">400</span> / <span id="compteur-limite">400</span>
</div> </div>
<!-- Booth : code + QR en bas a gauche -->
<div id="booth-info" class="booth-info cache">
<img id="booth-qr" class="booth-qr" src="">
<span id="booth-code" class="booth-code"></span>
</div>
<!-- Icone admin coin bas-droit --> <!-- Icone admin coin bas-droit -->
<div id="btn-admin" class="btn-admin" onclick="ouvrirAdmin()">&#9881;</div> <div id="btn-admin" class="btn-admin" onclick="ouvrirAdmin()">&#9881;</div>
</section> </section>
@@ -33,12 +38,12 @@
<h2>Choisissez votre mode</h2> <h2>Choisissez votre mode</h2>
<div class="grille-modes"> <div class="grille-modes">
<button class="btn-mode" data-mode="simple"> <button class="btn-mode" data-mode="simple">
<div class="mode-icone">&#128247;</div> <div class="mode-icone mode-icone-large">&#128247;</div>
<span>Photo</span> <span>Photo</span>
</button> </button>
<button class="btn-mode" data-mode="multi" id="btn-multi"> <button class="btn-mode" data-mode="multi" id="btn-multi">
<div class="mode-icone">&#127924;</div> <div class="mode-icone mode-icone-large">&#127902;</div>
<span>Multi-shot</span> <span>Pellicule</span>
</button> </button>
</div> </div>
<button class="btn-retour" onclick="allerA('accueil')">Retour</button> <button class="btn-retour" onclick="allerA('accueil')">Retour</button>

View File

@@ -14,6 +14,7 @@ async function init() {
setupEcranAccueil(); setupEcranAccueil();
setupModes(); setupModes();
setupOnglets(); setupOnglets();
chargerBoothInfo();
} }
function appliquerConfig() { function appliquerConfig() {
@@ -209,6 +210,28 @@ function toggleVisible(id, visible) {
} }
} }
async function chargerBoothInfo() {
const booth = config.booth || {};
const el = document.getElementById('booth-info');
if (!booth.actif || !booth.url) {
el.classList.add('cache');
return;
}
try {
const info = await apiGet('/api/booth/info');
if (info.password) {
document.getElementById('booth-code').textContent = info.password;
// QR code vers le site booth
const eventId = booth.event_id || 'default';
const qrUrl = `${booth.url}/${eventId}`;
document.getElementById('booth-qr').src = `/api/qr?url=${encodeURIComponent(qrUrl)}`;
el.classList.remove('cache');
}
} catch (e) {
el.classList.add('cache');
}
}
function afficherStatut(message, type = 'succes') { function afficherStatut(message, type = 'succes') {
const el = document.getElementById('statut-partage'); const el = document.getElementById('statut-partage');
el.textContent = message; el.textContent = message;