Files
photobooth/frontend/js/app.js
2026-06-27 15:46:38 +02:00

548 lines
16 KiB
JavaScript

/* Application principale - Routeur SPA et logique globale */
let config = {};
let modeActuel = 'simple';
let photosSession = []; // Photos de la session en cours
let photoFinale = null; // Photo finale (avec effets)
let ecranActuel = 'accueil';
// --- Initialisation ---
async function init() {
config = await apiGet('/api/config');
appliquerConfig();
setupEcranAccueil();
setupModes();
setupOnglets();
chargerBoothInfo();
verifierPhotostationDisponible();
}
function appliquerConfig() {
const event = config.evenement || {};
const fonc = config.fonctionnalites || {};
// Theme
const theme = event.theme || 'base';
document.documentElement.setAttribute('data-theme', theme);
// Couleurs (seulement si theme base, sinon le theme CSS gere)
if (theme === 'base') {
document.documentElement.style.setProperty('--primaire', event.couleur_primaire || '#e91e63');
document.documentElement.style.setProperty('--secondaire', event.couleur_secondaire || '#ffffff');
}
// Logo selon le theme
const logo = document.getElementById('accueil-logo');
if (logo) {
const logos = { sikapics: '/assets/logos/sikapics.png' };
if (logos[theme]) {
logo.src = logos[theme];
logo.classList.remove('cache');
} else {
logo.classList.add('cache');
}
}
// Nom evenement
const h1 = document.getElementById('nom-evenement');
if (h1) h1.textContent = event.nom || 'Photobooth';
// Visibilite des modes (restreint par formats evenement)
const fmtsEvent = event.formats_actifs;
const stripDispo = !fmtsEvent || fmtsEvent.includes('strip');
toggleVisible('btn-multi', fonc.multi_shot && stripDispo);
toggleVisible('btn-imprimer', fonc.impression);
toggleVisible('btn-email', fonc.email);
const boothActif = config.booth?.actif;
toggleVisible('btn-qr', fonc.qr_code || boothActif);
// Media d'accueil (video/gif en boucle)
appliquerMediaAccueil(event.media_accueil);
}
function appliquerMediaAccueil(media) {
const video = document.getElementById('accueil-video');
const gif = document.getElementById('accueil-gif');
const animDefaut = document.getElementById('accueil-animation-defaut');
// Reset
video.classList.add('cache');
gif.classList.add('cache');
video.pause();
video.src = '';
gif.src = '';
if (!media) {
// Pas de media, afficher l'animation par defaut
animDefaut.classList.remove('cache');
return;
}
animDefaut.classList.add('cache');
const ext = media.split('.').pop().toLowerCase();
if (ext === 'gif') {
gif.src = `/assets/animations/${media}`;
gif.classList.remove('cache');
} else {
video.src = `/assets/animations/${media}`;
video.classList.remove('cache');
video.play().catch(() => {});
}
}
// --- Navigation ---
function allerA(ecran) {
const tous = document.querySelectorAll('.ecran');
tous.forEach(e => e.classList.remove('actif'));
const cible = document.getElementById('ecran-' + ecran);
if (cible) {
cible.classList.add('actif');
ecranActuel = ecran;
}
// Masquer le popup QR a chaque changement d'ecran
fermerQR();
// Actions au changement d'ecran
if (ecran === 'accueil') {
photosSession = [];
photoFinale = null;
arreterPreview();
majCompteurAccueil();
} else if (ecran === 'capture') {
lancerCapture();
} else if (ecran === 'partage') {
majBoutonImprimerCompteur();
} else if (ecran === 'galerie') {
chargerGalerie();
} else if (ecran === 'admin') {
chargerAdmin();
demarrerTimeoutAdmin();
} else {
arreterTimeoutAdmin();
}
}
function demarrerTimeoutAdmin() {}
function arreterTimeoutAdmin() {}
function recommencer() {
photosSession = [];
photoFinale = null;
allerA('mode');
}
// --- Ecran accueil ---
function setupEcranAccueil() {
const accueil = document.getElementById('ecran-accueil');
// Toucher pour commencer (ignorer le bouton admin et le popup)
accueil.addEventListener('click', (e) => {
if (e.target.closest('.btn-admin')) return;
if (e.target.closest('.btn-photostation')) return;
allerA('mode');
});
}
// --- Admin : icone + mot de passe ---
const MDP_ADMIN = '1234';
const PIN_PHOTOSTATION = '1234';
let _pinPhotostationSaisi = '';
function ouvrirAdmin() {
const popup = document.getElementById('popup-mdp');
const input = document.getElementById('input-mdp-admin');
popup.classList.remove('cache');
input.value = '';
document.getElementById('mdp-erreur').classList.add('cache');
}
function fermerPopupMdp() {
document.getElementById('popup-mdp').classList.add('cache');
document.getElementById('input-mdp-admin').value = '';
}
function paveTouche(chiffre) {
const input = document.getElementById('input-mdp-admin');
input.value += chiffre;
}
function paveEffacer() {
const input = document.getElementById('input-mdp-admin');
input.value = input.value.slice(0, -1);
}
// --- Photostation ---
async function verifierPhotostationDisponible() {
const data = await apiGet('/api/photostation/disponible').catch(() => null);
if (data?.disponible) {
document.getElementById('btn-photostation').classList.remove('cache');
}
}
function ouvrirPhotostation() {
_pinPhotostationSaisi = '';
_majAffichagePin();
document.getElementById('pin-photostation-erreur').classList.add('cache');
document.getElementById('popup-pin-photostation').classList.remove('cache');
}
function fermerPopupPhotostation() {
document.getElementById('popup-pin-photostation').classList.add('cache');
_pinPhotostationSaisi = '';
}
function pinPhotostationTouche(c) {
if (_pinPhotostationSaisi.length >= 4) return;
_pinPhotostationSaisi += c;
_majAffichagePin();
if (_pinPhotostationSaisi.length === 4) {
setTimeout(_validerPinPhotostation, 150);
}
}
function pinPhotostationEffacer() {
_pinPhotostationSaisi = _pinPhotostationSaisi.slice(0, -1);
_majAffichagePin();
}
function _majAffichagePin() {
const n = _pinPhotostationSaisi.length;
const el = document.getElementById('pin-photostation-affichage');
el.textContent = '●'.repeat(n) + '○'.repeat(4 - n);
}
async function _validerPinPhotostation() {
if (_pinPhotostationSaisi !== PIN_PHOTOSTATION) {
document.getElementById('pin-photostation-erreur').classList.remove('cache');
_pinPhotostationSaisi = '';
_majAffichagePin();
return;
}
fermerPopupPhotostation();
afficherStatut('Lancement de la station d\'impression...', 'succes');
await apiPost('/api/photostation/lancer', {});
}
function validerMdpAdmin() {
const input = document.getElementById('input-mdp-admin');
if (input.value === MDP_ADMIN) {
fermerPopupMdp();
allerA('admin-choix');
} else {
document.getElementById('mdp-erreur').classList.remove('cache');
input.value = '';
}
}
function ouvrirWizard() {
allerA('wizard');
wizardInit();
}
// Valider avec Entree
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !document.getElementById('popup-mdp').classList.contains('cache')) {
validerMdpAdmin();
}
});
// --- Modes ---
let cadreChoisi = null; // cadre sélectionné par l'utilisateur avant capture
function setupModes() {
document.querySelectorAll('#ecran-mode .btn-mode').forEach(btn => {
btn.addEventListener('click', () => {
modeActuel = btn.dataset.mode;
cadreChoisi = null;
allerA('capture');
});
});
}
async function chargerCadresChoix() {
const cfg = await apiGet('/api/config');
const fmt = (cfg.impression?.format || '15x20').replace('-2up', '');
const data = await apiGet(`/api/cadres-impression/${fmt}`).catch(() => null);
if (!data || data.disponibles.length === 0) return false;
const grille = document.getElementById('grille-cadres-choix');
// Conserver "Sans cadre" en premier, supprimer les anciens cadres
const aucun = grille.querySelector('.cadre-choix-aucun');
grille.innerHTML = '';
if (aucun) grille.appendChild(aucun);
for (const nom of data.disponibles) {
const div = document.createElement('div');
div.className = 'cadre-choix-item';
div.innerHTML = `<div class="cadre-thumb-wrap"><img src="/assets/cadres/${fmt}/${nom}" alt="${nom}"></div><span>${nom.replace('.png','')}</span>`;
div.onclick = () => {
cadreChoisi = nom;
allerA('capture');
};
grille.appendChild(div);
}
return true;
}
function lancerSansCadre() {
cadreChoisi = null;
allerA('capture');
}
// --- Onglets admin ---
function setupOnglets() {
document.querySelectorAll('.onglet').forEach(onglet => {
onglet.addEventListener('click', () => {
document.querySelectorAll('.onglet').forEach(o => o.classList.remove('actif'));
document.querySelectorAll('.admin-panneau').forEach(p => p.classList.remove('actif'));
onglet.classList.add('actif');
const panneau = document.getElementById('panneau-' + onglet.dataset.onglet);
if (panneau) panneau.classList.add('actif');
});
});
}
// --- Utilitaires ---
async function apiGet(url) {
const resp = await fetch(url);
return resp.json();
}
async function apiPost(url, data = {}) {
const resp = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return resp.json();
}
function toggleVisible(id, visible) {
const el = document.getElementById(id);
if (el) {
if (visible === false) el.classList.add('cache');
else el.classList.remove('cache');
}
}
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) {
const eventId = (config.evenement || {}).event_id || booth.event_id || 'default';
const qrUrl = `${booth.url}/g/${eventId}?p=${info.password}`;
document.getElementById('booth-qr').src = `/api/qr?url=${encodeURIComponent(qrUrl)}`;
el.classList.remove('cache');
}
} catch (e) {
el.classList.add('cache');
}
}
function fermerQR() {
document.getElementById('popup-qr').classList.add('cache');
}
function afficherStatut(message, type = 'succes') {
const el = document.getElementById('statut-partage');
el.textContent = message;
el.className = 'statut-partage ' + type;
el.classList.remove('cache');
setTimeout(() => el.classList.add('cache'), 4000);
}
// WebSocket : mise a jour config en temps reel
wsOnMessage('config_maj', (msg) => {
config = msg.config;
appliquerConfig();
});
// Erreur camera : overlay visible sur tous les ecrans, reconnexion automatique cote backend
let _erreurCameraTimer = null;
wsOnMessage('camera_erreur', () => {
if (window.location.pathname === '/admin') return;
document.getElementById('camera-erreur').classList.remove('cache');
// Reload de dernier recours si la camera ne revient pas apres 2 min
if (_erreurCameraTimer) clearTimeout(_erreurCameraTimer);
_erreurCameraTimer = setTimeout(() => { location.reload(); }, 120000);
});
wsOnMessage('camera_ok', () => {
document.getElementById('camera-erreur').classList.add('cache');
if (_erreurCameraTimer) { clearTimeout(_erreurCameraTimer); _erreurCameraTimer = null; }
});
// Surveillance imprimante — poll toutes les 30s
function _afficherErreurImprimante(msg) {
const el = document.getElementById('printer-erreur');
document.getElementById('printer-erreur-msg').textContent = msg;
el.classList.remove('cache');
}
async function _pollStatutImprimante() {
try {
const r = await apiGet('/api/imprimante/statut-detail');
const el = document.getElementById('printer-erreur');
if (r.derniere_erreur) {
let msg = '';
const e = r.derniere_erreur.toLowerCase();
if (e.includes('media') && e.includes('match'))
msg = '⚠ Imprimante : format papier incorrect — vérifiez la cassette';
else if (e.includes('jam'))
msg = '⚠ Imprimante : bourrage papier — retirez le papier bloqué';
else if (e.includes('cancel'))
msg = '⚠ Imprimante : job annulé — ' + r.derniere_erreur.split(']').pop().trim();
if (msg) { _afficherErreurImprimante(msg); return; }
}
if (r.statut && (r.statut.includes('stopped') || r.statut.includes('disabled'))) {
_afficherErreurImprimante('⚠ Imprimante arrêtée — utilisez le bouton Évacuer dans l\'admin');
} else {
el.classList.add('cache');
}
} catch(e) {}
}
setInterval(_pollStatutImprimante, 30000);
// === WIZARD ===
let wizardStep = 1;
const WIZARD_TOTAL = 6;
let wizardData = {
nom: '',
theme: 'base',
modes: { simple: true, multi: true },
nbPhotos: 4,
fonctions: {},
};
function wizardInit() {
wizardStep = 1;
wizardData.nom = config.evenement?.nom || '';
wizardData.theme = config.evenement?.theme || 'base';
wizardData.nbPhotos = config.multi_shot?.nombre_photos || 4;
document.getElementById('wiz-nom').value = wizardData.nom;
document.getElementById('wiz-nb-photos').textContent = wizardData.nbPhotos;
wizardAfficherStep();
}
function wizardAfficherStep() {
document.querySelectorAll('.wizard-step').forEach(s => s.classList.remove('actif'));
const step = document.querySelector(`.wizard-step[data-step="${wizardStep}"]`);
if (step) step.classList.add('actif');
// Progress dots
const prog = document.getElementById('wizard-progress');
prog.innerHTML = '';
for (let i = 1; i <= WIZARD_TOTAL; i++) {
const dot = document.createElement('div');
dot.className = 'wizard-dot' + (i === wizardStep ? ' actif' : '') + (i < wizardStep ? ' fait' : '');
prog.appendChild(dot);
}
}
function wizardSuivant() {
// Sauvegarder l'etape courante
if (wizardStep === 1) wizardData.nom = document.getElementById('wiz-nom').value;
if (wizardStep === 4) wizardData.nbPhotos = parseInt(document.getElementById('wiz-nb-photos').textContent);
wizardStep++;
if (wizardStep > WIZARD_TOTAL) wizardStep = WIZARD_TOTAL;
// Resume
if (wizardStep === WIZARD_TOTAL) {
const modes = Object.keys(wizardData.modes).filter(k => wizardData.modes[k]);
const foncs = Object.keys(wizardData.fonctions).filter(k => wizardData.fonctions[k]);
document.getElementById('wiz-resume').innerHTML = `
<div><strong>Evenement :</strong> ${wizardData.nom || 'Photobooth'}</div>
<div><strong>Theme :</strong> ${wizardData.theme}</div>
<div><strong>Modes :</strong> ${modes.join(', ') || 'photo'}</div>
<div><strong>Photos/pellicule :</strong> ${wizardData.nbPhotos}</div>
<div><strong>Partage :</strong> ${foncs.join(', ') || 'aucun'}</div>
`;
}
wizardAfficherStep();
}
function wizardChoisir(btn, key) {
btn.closest('.wizard-choix').querySelectorAll('.wizard-card').forEach(c => c.classList.remove('actif'));
btn.classList.add('actif');
wizardData[key] = btn.dataset.val;
}
function wizardToggle(btn) {
btn.classList.toggle('actif');
const val = btn.dataset.val;
const isActive = btn.classList.contains('actif');
if (['simple', 'multi'].includes(val)) {
wizardData.modes[val] = isActive;
} else {
wizardData.fonctions[val] = isActive;
}
}
function wizardNbPhotos(delta) {
const el = document.getElementById('wiz-nb-photos');
let n = parseInt(el.textContent) + delta;
n = Math.max(2, Math.min(8, n));
el.textContent = n;
wizardData.nbPhotos = n;
}
async function wizardTerminer() {
// Sauvegarder toute la config
await apiPost('/api/config', {
evenement: {
nom: wizardData.nom || 'Photobooth',
theme: wizardData.theme,
},
fonctionnalites: {
photo_simple: wizardData.modes.simple !== false,
multi_shot: wizardData.modes.multi !== false,
impression: !!wizardData.fonctions.impression,
email: !!wizardData.fonctions.email,
qr_code: !!wizardData.fonctions.qr,
},
multi_shot: {
nombre_photos: wizardData.nbPhotos,
mode: 'strip',
},
booth: {
actif: !!wizardData.fonctions.galerie_live,
},
});
config = await apiGet('/api/config');
appliquerConfig();
chargerBoothInfo();
allerA('accueil');
}
// Demarrage
document.addEventListener('DOMContentLoaded', () => {
init().then(() => {
if (window.location.pathname === '/admin') {
allerA('admin-choix');
}
});
});