Backoffice complet : materiel, compteur, destinations, cadres, animations

- Onglet Materiel : choix appareil photo + imprimante
- Compteur avec limite configurable (399/400) affiche sur l'accueil
- Destinations multiples : memoire interne, cle USB, FTP, site web, email
- Choix sauvegarder toutes les photos ou seulement les imprimees
- Gestion des cadres : activation/desactivation + import
- 7 animations de compte a rebours : classique, explosion, rebond,
  rotation 3D, fondu, emoji fun, tremblement
- Preview des animations dans le backoffice
- Module destinations.py (copie USB, envoi FTP, compteur)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-21 23:03:30 +01:00
parent 3b3271a32f
commit d9de999d74
8 changed files with 1009 additions and 148 deletions

View File

@@ -1,6 +1,6 @@
/* Module administration - Menu cache */
/* Module administration - Backoffice complet */
// Mapping toggle ID -> cle config
// Mapping toggle ID -> cle config fonctionnalites
const TOGGLES_MAP = {
'tog-photo-simple': 'photo_simple',
'tog-multi-shot': 'multi_shot',
@@ -11,36 +11,53 @@ const TOGGLES_MAP = {
'tog-email': 'email',
'tog-qr-code': 'qr_code',
'tog-galerie': 'galerie',
'tog-compteur': 'compteur',
};
async function chargerAdmin() {
config = await apiGet('/api/config');
const fonc = config.fonctionnalites || {};
const event = config.evenement || {};
chargerMateriel();
chargerCompteur();
chargerDestinations();
chargerCadres();
chargerAnimations();
chargerFonctionnalites();
chargerEmailAdmin();
chargerGalerieAdmin();
}
// === MATERIEL ===
async function chargerMateriel() {
const cam = config.camera || {};
const imp = config.impression || {};
const email = config.email || {};
const qr = config.qr_code || {};
// Toggles fonctionnalites
for (const [id, cle] of Object.entries(TOGGLES_MAP)) {
const el = document.getElementById(id);
if (el) el.checked = fonc[cle] !== false;
}
// Evenement
setValue('admin-nom-event', event.nom);
setValue('admin-couleur-primaire', event.couleur_primaire || '#e91e63');
setValue('admin-couleur-secondaire', event.couleur_secondaire || '#ffffff');
// Camera
const statut = await apiGet('/api/camera/statut');
document.getElementById('admin-camera-statut').textContent =
statut.connectee ? 'Connectee' : 'Deconnectee';
const badge = document.getElementById('admin-camera-statut');
badge.textContent = statut.connectee ? 'Connectee' : 'Deconnectee';
badge.style.background = statut.connectee ? 'rgba(76,175,80,0.2)' : 'rgba(244,67,54,0.2)';
badge.style.color = statut.connectee ? 'var(--succes)' : 'var(--danger)';
// Liste appareils
const selectApp = document.getElementById('admin-appareil');
selectApp.innerHTML = '';
for (const a of statut.appareils) {
const opt = document.createElement('option');
opt.value = a;
opt.textContent = a;
selectApp.appendChild(opt);
}
if (cam.appareil) selectApp.value = cam.appareil;
setValue('admin-car', cam.compte_a_rebours || 3);
// Impression
// Imprimantes
await rafraichirImprimantes();
if (imp.imprimante) document.getElementById('admin-imprimante').value = imp.imprimante;
setValue('admin-copies', imp.copies || 1);
}
async function rafraichirImprimantes() {
const imprimantes = await apiGet('/api/imprimantes');
const select = document.getElementById('admin-imprimante');
select.innerHTML = '';
@@ -50,23 +67,266 @@ async function chargerAdmin() {
opt.textContent = `${p.nom} (${p.statut})`;
select.appendChild(opt);
}
if (imp.imprimante) select.value = imp.imprimante;
setValue('admin-copies', imp.copies || 1);
}
// Email
setValue('admin-smtp-host', email.smtp_host);
setValue('admin-smtp-port', email.smtp_port || 587);
setValue('admin-smtp-user', email.smtp_user);
setValue('admin-smtp-pass', email.smtp_password);
setValue('admin-expediteur', email.expediteur);
async function reconnecterCamera() {
const resultat = await apiPost('/api/camera/reconnecter');
chargerMateriel();
}
// Galerie
const compteur = await apiGet('/api/galerie/compteur');
document.getElementById('admin-nb-photos').textContent = compteur.photos_prises || 0;
document.getElementById('admin-nb-exports').textContent = compteur.exports || 0;
setValue('admin-url-galerie', qr.url_galerie);
async function sauvegarderMateriel() {
await apiPost('/api/config', {
camera: {
appareil: getValue('admin-appareil'),
compte_a_rebours: parseInt(getValue('admin-car')) || 3,
},
impression: {
imprimante: getValue('admin-imprimante'),
copies: parseInt(getValue('admin-copies')) || 1,
},
});
afficherStatut('Materiel sauvegarde', 'succes');
}
// Ecouter les changements de toggles
// === COMPTEUR ===
async function chargerCompteur() {
const etat = await apiGet('/api/compteur');
document.getElementById('tog-compteur-actif').checked = etat.actif;
document.getElementById('admin-compteur-restant').textContent = etat.restantes;
document.getElementById('admin-compteur-limite-display').textContent = etat.limite;
setValue('admin-compteur-limite', etat.limite);
}
async function sauvegarderCompteur() {
await apiPost('/api/config', {
compteur: {
actif: document.getElementById('tog-compteur-actif').checked,
limite: parseInt(getValue('admin-compteur-limite')) || 400,
},
});
chargerCompteur();
afficherStatut('Compteur sauvegarde', 'succes');
}
async function resetCompteur() {
if (confirm('Remettre le compteur a zero ?')) {
await apiPost('/api/compteur/reset');
chargerCompteur();
afficherStatut('Compteur remis a zero', 'succes');
}
}
// === DESTINATIONS ===
function chargerDestinations() {
const dest = config.destinations || {};
document.getElementById('tog-dest-memoire').checked = dest.memoire_interne !== false;
document.getElementById('tog-dest-usb').checked = dest.cle_usb || false;
document.getElementById('tog-dest-ftp').checked = dest.ftp || false;
document.getElementById('tog-dest-web').checked = dest.site_web || false;
document.getElementById('tog-dest-email-auto').checked = dest.email_auto || false;
// Sous-configs visibles si actives
toggleSousConfig('tog-dest-usb', 'dest-usb-details');
toggleSousConfig('tog-dest-ftp', 'dest-ftp-details');
toggleSousConfig('tog-dest-web', 'dest-web-details');
setValue('admin-ftp-host', dest.ftp_host);
setValue('admin-ftp-port', dest.ftp_port || 21);
setValue('admin-ftp-user', dest.ftp_user);
setValue('admin-ftp-pass', dest.ftp_password);
setValue('admin-ftp-chemin', dest.ftp_chemin || '/photobooth');
setValue('admin-web-url', dest.site_web_url);
// Radio sauvegarder tout / imprimees
if (dest.sauvegarder_tout === false) {
document.getElementById('sauv-imprimees').checked = true;
} else {
document.getElementById('sauv-tout').checked = true;
}
// Listeners pour afficher/cacher sous-configs
['tog-dest-usb', 'tog-dest-ftp', 'tog-dest-web'].forEach(id => {
const el = document.getElementById(id);
el.onchange = () => {
const map = { 'tog-dest-usb': 'dest-usb-details', 'tog-dest-ftp': 'dest-ftp-details', 'tog-dest-web': 'dest-web-details' };
toggleSousConfig(id, map[id]);
};
});
}
function toggleSousConfig(toggleId, detailsId) {
const checked = document.getElementById(toggleId).checked;
const el = document.getElementById(detailsId);
if (checked) el.classList.remove('cache');
else el.classList.add('cache');
}
async function detecterUSB() {
const usbs = await apiGet('/api/usb/detecter');
const select = document.getElementById('admin-chemin-usb');
select.innerHTML = '';
if (usbs.length === 0) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'Aucune cle detectee';
select.appendChild(opt);
} else {
for (const u of usbs) {
const opt = document.createElement('option');
opt.value = u;
opt.textContent = u;
select.appendChild(opt);
}
}
}
async function sauvegarderDestinations() {
await apiPost('/api/config', {
destinations: {
memoire_interne: document.getElementById('tog-dest-memoire').checked,
cle_usb: document.getElementById('tog-dest-usb').checked,
chemin_usb: getValue('admin-chemin-usb') || '/media/usb',
ftp: document.getElementById('tog-dest-ftp').checked,
ftp_host: getValue('admin-ftp-host'),
ftp_port: parseInt(getValue('admin-ftp-port')) || 21,
ftp_user: getValue('admin-ftp-user'),
ftp_password: getValue('admin-ftp-pass'),
ftp_chemin: getValue('admin-ftp-chemin') || '/photobooth',
site_web: document.getElementById('tog-dest-web').checked,
site_web_url: getValue('admin-web-url'),
email_auto: document.getElementById('tog-dest-email-auto').checked,
sauvegarder_tout: document.getElementById('sauv-tout').checked,
},
});
afficherStatut('Destinations sauvegardees', 'succes');
}
// === CADRES ===
async function chargerCadres() {
const data = await apiGet('/api/cadres');
const liste = document.getElementById('liste-cadres');
liste.innerHTML = '';
if (data.tous.length === 0) {
liste.innerHTML = '<p class="texte-secondaire">Aucun cadre importe</p>';
return;
}
for (const nom of data.tous) {
const div = document.createElement('div');
div.className = 'cadre-item';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = data.actifs.includes(nom);
cb.onchange = () => majCadresActifs();
const img = document.createElement('img');
img.src = `/assets/overlays/${nom}`;
img.alt = nom;
const span = document.createElement('span');
span.textContent = nom.replace('.png', '');
div.appendChild(cb);
div.appendChild(img);
div.appendChild(span);
liste.appendChild(div);
}
}
async function majCadresActifs() {
const items = document.querySelectorAll('.cadre-item input[type="checkbox"]');
const noms = document.querySelectorAll('.cadre-item span');
const actifs = [];
items.forEach((cb, i) => {
if (cb.checked) actifs.push(noms[i].textContent + '.png');
});
await apiPost('/api/cadres', { actifs });
}
async function uploaderCadre() {
const input = document.getElementById('input-cadre');
if (!input.files.length) return;
const formData = new FormData();
formData.append('fichier', input.files[0]);
await fetch('/api/upload/overlay', { method: 'POST', body: formData });
input.value = '';
chargerCadres();
afficherStatut('Cadre importe', 'succes');
}
// === ANIMATIONS ===
let animationChoisie = 'classique';
async function chargerAnimations() {
const anims = await apiGet('/api/animations');
animationChoisie = config.camera?.animation_compte_a_rebours || 'classique';
const liste = document.getElementById('liste-animations');
liste.innerHTML = '';
for (const [id, nom] of Object.entries(anims)) {
const div = document.createElement('div');
div.className = 'anim-option' + (id === animationChoisie ? ' actif' : '');
div.textContent = nom;
div.onclick = () => choisirAnimation(id, div);
liste.appendChild(div);
}
}
async function choisirAnimation(id, el) {
animationChoisie = id;
document.querySelectorAll('.anim-option').forEach(e => e.classList.remove('actif'));
el.classList.add('actif');
await apiPost('/api/config', {
camera: { animation_compte_a_rebours: id },
});
testerAnimation();
}
const EMOJI_MAP = { 3: '🤪', 2: '😱', 1: '🔥' };
function testerAnimation() {
const chiffre = document.getElementById('preview-anim-chiffre');
let count = 3;
function afficher() {
if (animationChoisie === 'emoji') {
chiffre.textContent = EMOJI_MAP[count] || count;
} else {
chiffre.textContent = count;
}
chiffre.className = 'anim-chiffre anim-' + animationChoisie;
// Force reflow pour relancer l'animation
chiffre.style.animation = 'none';
chiffre.offsetHeight;
chiffre.className = 'anim-chiffre anim-' + animationChoisie;
count--;
if (count > 0) setTimeout(afficher, 800);
}
afficher();
}
// === FONCTIONNALITES ===
function chargerFonctionnalites() {
const fonc = config.fonctionnalites || {};
for (const [id, cle] of Object.entries(TOGGLES_MAP)) {
const el = document.getElementById(id);
if (el) el.checked = fonc[cle] !== false;
}
setupToggleListeners();
}
@@ -74,7 +334,6 @@ function setupToggleListeners() {
for (const [id, cle] of Object.entries(TOGGLES_MAP)) {
const el = document.getElementById(id);
if (!el) continue;
// Retirer les anciens listeners en clonant
const nouveau = el.cloneNode(true);
el.parentNode.replaceChild(nouveau, el);
nouveau.addEventListener('change', () => {
@@ -83,6 +342,15 @@ function setupToggleListeners() {
}
}
// === EVENEMENT ===
function chargerEvenementAdmin() {
const event = config.evenement || {};
setValue('admin-nom-event', event.nom);
setValue('admin-couleur-primaire', event.couleur_primaire || '#e91e63');
setValue('admin-couleur-secondaire', event.couleur_secondaire || '#ffffff');
}
async function sauvegarderEvenement() {
await apiPost('/api/config', {
evenement: {
@@ -90,13 +358,21 @@ async function sauvegarderEvenement() {
couleur_primaire: getValue('admin-couleur-primaire'),
couleur_secondaire: getValue('admin-couleur-secondaire'),
},
camera: {
compte_a_rebours: parseInt(getValue('admin-car')) || 3,
},
});
afficherStatut('Evenement sauvegarde', 'succes');
}
// === EMAIL ===
function chargerEmailAdmin() {
const email = config.email || {};
setValue('admin-smtp-host', email.smtp_host);
setValue('admin-smtp-port', email.smtp_port || 587);
setValue('admin-smtp-user', email.smtp_user);
setValue('admin-smtp-pass', email.smtp_password);
setValue('admin-expediteur', email.expediteur);
}
async function sauvegarderEmail() {
await apiPost('/api/config', {
email: {
@@ -110,45 +386,43 @@ async function sauvegarderEmail() {
afficherStatut('Email sauvegarde', 'succes');
}
// === GALERIE ===
async function chargerGalerieAdmin() {
const compteur = await apiGet('/api/galerie/compteur');
document.getElementById('admin-nb-photos').textContent = compteur.photos_prises || 0;
document.getElementById('admin-nb-exports').textContent = compteur.exports || 0;
setValue('admin-url-galerie', (config.qr_code || {}).url_galerie);
}
async function sauvegarderGalerie() {
await apiPost('/api/config', {
qr_code: { url_galerie: getValue('admin-url-galerie') },
impression: {
imprimante: getValue('admin-imprimante'),
copies: parseInt(getValue('admin-copies')) || 1,
},
});
afficherStatut('Configuration sauvegardee', 'succes');
}
async function reconnecterCamera() {
const resultat = await apiPost('/api/camera/reconnecter');
document.getElementById('admin-camera-statut').textContent =
resultat.connectee ? 'Connectee' : 'Deconnectee';
}
function confirmerViderGalerie() {
if (confirm('Supprimer TOUTES les photos ? Cette action est irreversible.')) {
apiPost('/api/galerie/vider').then(() => {
afficherStatut('Galerie videe', 'succes');
chargerAdmin();
chargerGalerieAdmin();
});
}
}
// === SYSTEME ===
function confirmerRedemarrage() {
if (confirm('Redemarrer le systeme ?')) {
apiPost('/api/systeme/redemarrer');
}
if (confirm('Redemarrer le systeme ?')) apiPost('/api/systeme/redemarrer');
}
function confirmerExtinction() {
if (confirm('Eteindre le systeme ?')) {
apiPost('/api/systeme/eteindre');
}
if (confirm('Eteindre le systeme ?')) apiPost('/api/systeme/eteindre');
}
// Utilitaires
// === Utilitaires ===
function setValue(id, val) {
const el = document.getElementById(id);
if (el) el.value = val || '';

View File

@@ -52,6 +52,7 @@ function allerA(ecran) {
photosSession = [];
photoFinale = null;
arreterPreview();
majCompteurAccueil();
} else if (ecran === 'capture') {
lancerCapture();
} else if (ecran === 'galerie') {

View File

@@ -3,16 +3,17 @@
let previewActif = false;
let previewInterval = null;
const EMOJI_CAR = { 3: '🤪', 2: '😱', 1: '🔥' };
// --- Preview live ---
function lancerPreview() {
if (previewActif) return;
previewActif = true;
// Demander des previews via WebSocket
previewInterval = setInterval(() => {
if (previewActif) wsEnvoyer({ type: 'preview' });
}, 100); // ~10 fps
}, 100);
}
function arreterPreview() {
@@ -23,7 +24,6 @@ function arreterPreview() {
}
}
// Recevoir les previews
wsOnMessage('preview', (msg) => {
if (!previewActif) return;
const img = document.getElementById('img-preview');
@@ -35,6 +35,7 @@ wsOnMessage('preview', (msg) => {
async function lancerCapture() {
photosSession = [];
lancerPreview();
majCompteurAccueil();
const nbPhotos = getNombrePhotos();
const compteurEl = document.getElementById('capture-compteur');
@@ -44,27 +45,28 @@ async function lancerCapture() {
compteurEl.textContent = `Photo ${i + 1} / ${nbPhotos}`;
}
// Compte a rebours
await compteARebours();
// Flash
afficherFlash();
// Capture
arreterPreview();
const resultat = await apiPost('/api/capturer');
if (resultat.erreur) {
// Limite atteinte ou erreur
allerA('accueil');
return;
}
if (resultat.nom) {
photosSession.push(resultat.nom);
}
// Reprendre le preview si encore des photos a prendre
if (i < nbPhotos - 1) {
lancerPreview();
await pause(500);
}
}
// Traitement selon le mode
await traiterCapture();
}
@@ -78,15 +80,24 @@ async function compteARebours() {
const conteneur = document.getElementById('compte-a-rebours');
const chiffre = document.getElementById('chiffre-car');
const duree = config.camera?.compte_a_rebours || 3;
const anim = config.camera?.animation_compte_a_rebours || 'classique';
conteneur.classList.remove('cache');
for (let i = duree; i > 0; i--) {
chiffre.textContent = i;
// Re-trigger animation
// Contenu selon le type d'animation
if (anim === 'emoji') {
chiffre.textContent = EMOJI_CAR[i] || i;
} else {
chiffre.textContent = i;
}
// Appliquer l'animation
chiffre.className = 'anim-chiffre anim-' + anim;
chiffre.style.animation = 'none';
chiffre.offsetHeight; // force reflow
chiffre.style.animation = 'pop 0.5s ease';
chiffre.offsetHeight;
chiffre.className = 'anim-chiffre anim-' + anim;
await pause(1000);
}
@@ -109,7 +120,6 @@ async function traiterCapture() {
}
if (modeActuel === 'multi') {
// Creer le strip/collage
const mode = config.multi_shot?.mode || 'strip';
let resultat;
if (mode === 'strip') {
@@ -122,7 +132,6 @@ async function traiterCapture() {
afficherPreviewPhoto('/data/exports/' + resultat.nom);
}
} else {
// Photo simple - aller au preview avec filtres
photoFinale = photosSession[0];
afficherPreviewPhoto('/data/photos/' + photosSession[0]);
}
@@ -134,17 +143,28 @@ function afficherPreviewPhoto(chemin) {
document.getElementById('photo-resultat').src = chemin;
document.getElementById('photo-partage').src = chemin;
// Charger les filtres si photo simple
if (modeActuel === 'simple' && config.fonctionnalites?.filtres) {
chargerFiltres();
}
// Charger les overlays
if (config.fonctionnalites?.overlays) {
chargerOverlays();
}
}
// --- Compteur accueil ---
async function majCompteurAccueil() {
const etat = await apiGet('/api/compteur');
const el = document.getElementById('compteur-accueil');
if (etat.actif) {
el.classList.remove('cache');
document.getElementById('compteur-restant').textContent = etat.restantes;
document.getElementById('compteur-limite').textContent = etat.limite;
} else {
el.classList.add('cache');
}
}
function pause(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}