Init photobooth - borne photo evenementielle RPi4
Backend FastAPI complet : camera gphoto2, filtres/overlays Pillow, chroma key OpenCV, multi-shot/collage, GIF, impression CUPS, email SMTP, QR code. Frontend web vanilla (HTML/CSS/JS) pour Chromium kiosk. Menu admin cache (appui long coin ecran). Toutes les fonctionnalites activables/desactivables. Scripts install + systemd pour RPi4. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
161
frontend/js/admin.js
Normal file
161
frontend/js/admin.js
Normal file
@@ -0,0 +1,161 @@
|
||||
/* Module administration - Menu cache */
|
||||
|
||||
// Mapping toggle ID -> cle config
|
||||
const TOGGLES_MAP = {
|
||||
'tog-photo-simple': 'photo_simple',
|
||||
'tog-multi-shot': 'multi_shot',
|
||||
'tog-gif': 'gif',
|
||||
'tog-filtres': 'filtres',
|
||||
'tog-overlays': 'overlays',
|
||||
'tog-chroma-key': 'chroma_key',
|
||||
'tog-impression': 'impression',
|
||||
'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 || {};
|
||||
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';
|
||||
setValue('admin-car', cam.compte_a_rebours || 3);
|
||||
|
||||
// Impression
|
||||
const imprimantes = await apiGet('/api/imprimantes');
|
||||
const select = document.getElementById('admin-imprimante');
|
||||
select.innerHTML = '';
|
||||
for (const p of imprimantes) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = p.nom;
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
// Ecouter les changements de toggles
|
||||
setupToggleListeners();
|
||||
}
|
||||
|
||||
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', () => {
|
||||
apiPost('/api/config', { fonctionnalites: { [cle]: nouveau.checked } });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function sauvegarderEvenement() {
|
||||
await apiPost('/api/config', {
|
||||
evenement: {
|
||||
nom: getValue('admin-nom-event'),
|
||||
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');
|
||||
}
|
||||
|
||||
async function sauvegarderEmail() {
|
||||
await apiPost('/api/config', {
|
||||
email: {
|
||||
smtp_host: getValue('admin-smtp-host'),
|
||||
smtp_port: parseInt(getValue('admin-smtp-port')) || 587,
|
||||
smtp_user: getValue('admin-smtp-user'),
|
||||
smtp_password: getValue('admin-smtp-pass'),
|
||||
expediteur: getValue('admin-expediteur'),
|
||||
},
|
||||
});
|
||||
afficherStatut('Email sauvegarde', 'succes');
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function confirmerRedemarrage() {
|
||||
if (confirm('Redemarrer le systeme ?')) {
|
||||
apiPost('/api/systeme/redemarrer');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmerExtinction() {
|
||||
if (confirm('Eteindre le systeme ?')) {
|
||||
apiPost('/api/systeme/eteindre');
|
||||
}
|
||||
}
|
||||
|
||||
// Utilitaires
|
||||
function setValue(id, val) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.value = val || '';
|
||||
}
|
||||
|
||||
function getValue(id) {
|
||||
const el = document.getElementById(id);
|
||||
return el ? el.value : '';
|
||||
}
|
||||
172
frontend/js/app.js
Normal file
172
frontend/js/app.js
Normal file
@@ -0,0 +1,172 @@
|
||||
/* 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();
|
||||
}
|
||||
|
||||
function appliquerConfig() {
|
||||
const event = config.evenement || {};
|
||||
const fonc = config.fonctionnalites || {};
|
||||
|
||||
// Couleurs
|
||||
document.documentElement.style.setProperty('--primaire', event.couleur_primaire || '#e91e63');
|
||||
document.documentElement.style.setProperty('--secondaire', event.couleur_secondaire || '#ffffff');
|
||||
|
||||
// Nom evenement
|
||||
const h1 = document.getElementById('nom-evenement');
|
||||
if (h1) h1.textContent = event.nom || 'Photobooth';
|
||||
|
||||
// Visibilite des modes
|
||||
toggleVisible('btn-multi', fonc.multi_shot);
|
||||
toggleVisible('btn-gif', fonc.gif);
|
||||
toggleVisible('btn-imprimer', fonc.impression);
|
||||
toggleVisible('btn-email', fonc.email);
|
||||
toggleVisible('btn-qr', fonc.qr_code);
|
||||
}
|
||||
|
||||
// --- 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;
|
||||
}
|
||||
|
||||
// Actions au changement d'ecran
|
||||
if (ecran === 'accueil') {
|
||||
photosSession = [];
|
||||
photoFinale = null;
|
||||
arreterPreview();
|
||||
} else if (ecran === 'capture') {
|
||||
lancerCapture();
|
||||
} else if (ecran === 'galerie') {
|
||||
chargerGalerie();
|
||||
} else if (ecran === 'admin') {
|
||||
chargerAdmin();
|
||||
}
|
||||
}
|
||||
|
||||
function recommencer() {
|
||||
photosSession = [];
|
||||
photoFinale = null;
|
||||
allerA('mode');
|
||||
}
|
||||
|
||||
// --- Ecran accueil ---
|
||||
|
||||
function setupEcranAccueil() {
|
||||
const accueil = document.getElementById('ecran-accueil');
|
||||
|
||||
// Toucher pour commencer
|
||||
accueil.addEventListener('click', (e) => {
|
||||
// Ignorer si c'est la zone admin
|
||||
if (e.target.closest('.zone-admin')) return;
|
||||
allerA('mode');
|
||||
});
|
||||
|
||||
// Zone admin : appui long 3s
|
||||
const zoneAdmin = document.getElementById('zone-admin');
|
||||
let timerAdmin = null;
|
||||
|
||||
zoneAdmin.addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
timerAdmin = setTimeout(() => allerA('admin'), 3000);
|
||||
});
|
||||
|
||||
zoneAdmin.addEventListener('touchend', () => {
|
||||
if (timerAdmin) clearTimeout(timerAdmin);
|
||||
});
|
||||
|
||||
// Support souris aussi (pour dev)
|
||||
zoneAdmin.addEventListener('mousedown', (e) => {
|
||||
e.stopPropagation();
|
||||
timerAdmin = setTimeout(() => allerA('admin'), 3000);
|
||||
});
|
||||
|
||||
zoneAdmin.addEventListener('mouseup', () => {
|
||||
if (timerAdmin) clearTimeout(timerAdmin);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Modes ---
|
||||
|
||||
function setupModes() {
|
||||
document.querySelectorAll('.btn-mode').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
modeActuel = btn.dataset.mode;
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
// Demarrage
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
158
frontend/js/camera.js
Normal file
158
frontend/js/camera.js
Normal file
@@ -0,0 +1,158 @@
|
||||
/* Module capture photo - Preview live, compte a rebours, declenchement */
|
||||
|
||||
let previewActif = false;
|
||||
let previewInterval = null;
|
||||
|
||||
// --- Preview live ---
|
||||
|
||||
function lancerPreview() {
|
||||
if (previewActif) return;
|
||||
previewActif = true;
|
||||
|
||||
// Demander des previews via WebSocket
|
||||
previewInterval = setInterval(() => {
|
||||
if (previewActif) wsEnvoyer({ type: 'preview' });
|
||||
}, 100); // ~10 fps
|
||||
}
|
||||
|
||||
function arreterPreview() {
|
||||
previewActif = false;
|
||||
if (previewInterval) {
|
||||
clearInterval(previewInterval);
|
||||
previewInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Recevoir les previews
|
||||
wsOnMessage('preview', (msg) => {
|
||||
if (!previewActif) return;
|
||||
const img = document.getElementById('img-preview');
|
||||
if (img) img.src = msg.image;
|
||||
});
|
||||
|
||||
// --- Capture ---
|
||||
|
||||
async function lancerCapture() {
|
||||
photosSession = [];
|
||||
lancerPreview();
|
||||
|
||||
const nbPhotos = getNombrePhotos();
|
||||
const compteurEl = document.getElementById('capture-compteur');
|
||||
|
||||
for (let i = 0; i < nbPhotos; i++) {
|
||||
if (nbPhotos > 1) {
|
||||
compteurEl.textContent = `Photo ${i + 1} / ${nbPhotos}`;
|
||||
}
|
||||
|
||||
// Compte a rebours
|
||||
await compteARebours();
|
||||
|
||||
// Flash
|
||||
afficherFlash();
|
||||
|
||||
// Capture
|
||||
arreterPreview();
|
||||
const resultat = await apiPost('/api/capturer');
|
||||
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();
|
||||
}
|
||||
|
||||
function getNombrePhotos() {
|
||||
if (modeActuel === 'simple') return 1;
|
||||
if (modeActuel === 'multi') return config.multi_shot?.nombre_photos || 3;
|
||||
if (modeActuel === 'gif') return config.gif?.nombre_frames || 4;
|
||||
return 1;
|
||||
}
|
||||
|
||||
async function compteARebours() {
|
||||
const conteneur = document.getElementById('compte-a-rebours');
|
||||
const chiffre = document.getElementById('chiffre-car');
|
||||
const duree = config.camera?.compte_a_rebours || 3;
|
||||
|
||||
conteneur.classList.remove('cache');
|
||||
|
||||
for (let i = duree; i > 0; i--) {
|
||||
chiffre.textContent = i;
|
||||
// Re-trigger animation
|
||||
chiffre.style.animation = 'none';
|
||||
chiffre.offsetHeight; // force reflow
|
||||
chiffre.style.animation = 'pop 0.5s ease';
|
||||
await pause(1000);
|
||||
}
|
||||
|
||||
conteneur.classList.add('cache');
|
||||
}
|
||||
|
||||
function afficherFlash() {
|
||||
const flash = document.getElementById('flash-blanc');
|
||||
flash.classList.remove('cache');
|
||||
flash.style.animation = 'none';
|
||||
flash.offsetHeight;
|
||||
flash.style.animation = 'flash 0.3s ease-out forwards';
|
||||
setTimeout(() => flash.classList.add('cache'), 400);
|
||||
}
|
||||
|
||||
async function traiterCapture() {
|
||||
if (photosSession.length === 0) {
|
||||
allerA('accueil');
|
||||
return;
|
||||
}
|
||||
|
||||
if (modeActuel === 'gif') {
|
||||
// Creer le GIF
|
||||
const resultat = await apiPost('/api/gif', { photos: photosSession });
|
||||
if (resultat.nom) {
|
||||
photoFinale = resultat.nom;
|
||||
afficherPreviewPhoto('/data/exports/' + resultat.nom);
|
||||
}
|
||||
} else if (modeActuel === 'multi') {
|
||||
// Creer le strip/collage
|
||||
const mode = config.multi_shot?.mode || 'strip';
|
||||
let resultat;
|
||||
if (mode === 'strip') {
|
||||
resultat = await apiPost('/api/strip', { photos: photosSession });
|
||||
} else {
|
||||
resultat = await apiPost('/api/collage', { photos: photosSession });
|
||||
}
|
||||
if (resultat.nom) {
|
||||
photoFinale = resultat.nom;
|
||||
afficherPreviewPhoto('/data/exports/' + resultat.nom);
|
||||
}
|
||||
} else {
|
||||
// Photo simple - aller au preview avec filtres
|
||||
photoFinale = photosSession[0];
|
||||
afficherPreviewPhoto('/data/photos/' + photosSession[0]);
|
||||
}
|
||||
|
||||
allerA('preview');
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
function pause(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
103
frontend/js/effects.js
vendored
Normal file
103
frontend/js/effects.js
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
/* Module effets - Filtres et overlays */
|
||||
|
||||
let filtreActuel = 'original';
|
||||
let overlayActuel = null;
|
||||
|
||||
async function chargerFiltres() {
|
||||
const filtres = await apiGet('/api/filtres');
|
||||
const barre = document.getElementById('barre-filtres');
|
||||
barre.innerHTML = '';
|
||||
|
||||
for (const [id, nom] of Object.entries(filtres)) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn-filtre' + (id === 'original' ? ' actif' : '');
|
||||
btn.textContent = nom;
|
||||
btn.addEventListener('click', () => appliquerFiltreUI(id, btn));
|
||||
barre.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
async function appliquerFiltreUI(filtre, btn) {
|
||||
// Activer visuellement
|
||||
document.querySelectorAll('.btn-filtre').forEach(b => b.classList.remove('actif'));
|
||||
btn.classList.add('actif');
|
||||
filtreActuel = filtre;
|
||||
|
||||
if (filtre === 'original') {
|
||||
const chemin = '/data/photos/' + photosSession[0];
|
||||
document.getElementById('photo-resultat').src = chemin;
|
||||
document.getElementById('photo-partage').src = chemin;
|
||||
photoFinale = photosSession[0];
|
||||
return;
|
||||
}
|
||||
|
||||
const resultat = await apiPost('/api/filtre', {
|
||||
photo: photosSession[0],
|
||||
filtre: filtre,
|
||||
});
|
||||
|
||||
if (resultat.nom) {
|
||||
photoFinale = resultat.nom;
|
||||
document.getElementById('photo-resultat').src = '/data/exports/' + resultat.nom;
|
||||
document.getElementById('photo-partage').src = '/data/exports/' + resultat.nom;
|
||||
}
|
||||
}
|
||||
|
||||
async function chargerOverlays() {
|
||||
const overlays = await apiGet('/api/overlays');
|
||||
const barre = document.getElementById('barre-overlays');
|
||||
|
||||
if (overlays.length === 0) {
|
||||
barre.classList.add('cache');
|
||||
return;
|
||||
}
|
||||
|
||||
barre.classList.remove('cache');
|
||||
barre.innerHTML = '';
|
||||
|
||||
// Bouton "sans overlay"
|
||||
const btnAucun = document.createElement('button');
|
||||
btnAucun.className = 'btn-overlay actif';
|
||||
btnAucun.textContent = 'Sans cadre';
|
||||
btnAucun.addEventListener('click', () => retirerOverlay(btnAucun));
|
||||
barre.appendChild(btnAucun);
|
||||
|
||||
for (const nom of overlays) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn-overlay';
|
||||
btn.textContent = nom.replace('.png', '');
|
||||
btn.addEventListener('click', () => appliquerOverlayUI(nom, btn));
|
||||
barre.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
async function appliquerOverlayUI(nom, btn) {
|
||||
document.querySelectorAll('.btn-overlay').forEach(b => b.classList.remove('actif'));
|
||||
btn.classList.add('actif');
|
||||
overlayActuel = nom;
|
||||
|
||||
const photo = photosSession[0];
|
||||
const resultat = await apiPost('/api/overlay', { photo, overlay: nom });
|
||||
|
||||
if (resultat.nom) {
|
||||
photoFinale = resultat.nom;
|
||||
document.getElementById('photo-resultat').src = '/data/exports/' + resultat.nom;
|
||||
document.getElementById('photo-partage').src = '/data/exports/' + resultat.nom;
|
||||
}
|
||||
}
|
||||
|
||||
function retirerOverlay(btn) {
|
||||
document.querySelectorAll('.btn-overlay').forEach(b => b.classList.remove('actif'));
|
||||
btn.classList.add('actif');
|
||||
overlayActuel = null;
|
||||
|
||||
// Revenir a la photo avec filtre actuel (ou originale)
|
||||
if (filtreActuel !== 'original') {
|
||||
appliquerFiltreUI(filtreActuel, document.querySelector('.btn-filtre.actif'));
|
||||
} else {
|
||||
const chemin = '/data/photos/' + photosSession[0];
|
||||
document.getElementById('photo-resultat').src = chemin;
|
||||
document.getElementById('photo-partage').src = chemin;
|
||||
photoFinale = photosSession[0];
|
||||
}
|
||||
}
|
||||
27
frontend/js/gallery.js
Normal file
27
frontend/js/gallery.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/* Module galerie */
|
||||
|
||||
async function chargerGalerie() {
|
||||
const photos = await apiGet('/api/galerie');
|
||||
const grille = document.getElementById('grille-galerie');
|
||||
grille.innerHTML = '';
|
||||
|
||||
if (photos.length === 0) {
|
||||
grille.innerHTML = '<p style="color:var(--texte-secondaire);grid-column:1/-1;text-align:center">Aucune photo pour le moment</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const photo of photos) {
|
||||
const img = document.createElement('img');
|
||||
img.src = '/' + photo.chemin;
|
||||
img.alt = photo.nom;
|
||||
img.loading = 'lazy';
|
||||
img.addEventListener('click', () => voirPhoto(photo));
|
||||
grille.appendChild(img);
|
||||
}
|
||||
}
|
||||
|
||||
function voirPhoto(photo) {
|
||||
photoFinale = photo.nom;
|
||||
document.getElementById('photo-partage').src = '/' + photo.chemin;
|
||||
allerA('partage');
|
||||
}
|
||||
51
frontend/js/share.js
Normal file
51
frontend/js/share.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/* Module partage - Impression, email, QR code */
|
||||
|
||||
async function lancerImpression() {
|
||||
if (!photoFinale) return;
|
||||
afficherStatut('Impression en cours...', 'succes');
|
||||
const resultat = await apiPost('/api/imprimer', { photo: photoFinale });
|
||||
if (resultat.succes) {
|
||||
afficherStatut('Photo envoyee a l\'imprimante !', 'succes');
|
||||
} else {
|
||||
afficherStatut('Erreur d\'impression', 'erreur');
|
||||
}
|
||||
}
|
||||
|
||||
function ouvrirEmail() {
|
||||
document.getElementById('form-email').classList.remove('cache');
|
||||
document.getElementById('input-email').focus();
|
||||
}
|
||||
|
||||
function fermerEmail() {
|
||||
document.getElementById('form-email').classList.add('cache');
|
||||
document.getElementById('input-email').value = '';
|
||||
}
|
||||
|
||||
async function envoyerEmail() {
|
||||
const email = document.getElementById('input-email').value.trim();
|
||||
if (!email || !photoFinale) return;
|
||||
|
||||
afficherStatut('Envoi en cours...', 'succes');
|
||||
const resultat = await apiPost('/api/email', { email, photo: photoFinale });
|
||||
if (resultat.succes) {
|
||||
afficherStatut('Email envoye !', 'succes');
|
||||
fermerEmail();
|
||||
} else {
|
||||
afficherStatut('Erreur d\'envoi email', 'erreur');
|
||||
}
|
||||
}
|
||||
|
||||
async function afficherQR() {
|
||||
const zone = document.getElementById('zone-qr');
|
||||
zone.classList.toggle('cache');
|
||||
|
||||
if (!zone.classList.contains('cache')) {
|
||||
const resultat = await apiGet('/api/qr/galerie');
|
||||
if (resultat.chemin) {
|
||||
document.getElementById('img-qr').src = resultat.chemin;
|
||||
} else {
|
||||
afficherStatut('QR Code non configure', 'erreur');
|
||||
zone.classList.add('cache');
|
||||
}
|
||||
}
|
||||
}
|
||||
48
frontend/js/websocket.js
Normal file
48
frontend/js/websocket.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/* Communication WebSocket avec le backend */
|
||||
|
||||
let ws = null;
|
||||
let wsReconnectTimer = null;
|
||||
const wsCallbacks = {};
|
||||
|
||||
function wsConnecter() {
|
||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
ws = new WebSocket(`${proto}//${location.host}/ws`);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connecte');
|
||||
if (wsReconnectTimer) {
|
||||
clearInterval(wsReconnectTimer);
|
||||
wsReconnectTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data);
|
||||
const cb = wsCallbacks[msg.type];
|
||||
if (cb) cb(msg);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket deconnecte, reconnexion dans 3s...');
|
||||
if (!wsReconnectTimer) {
|
||||
wsReconnectTimer = setInterval(wsConnecter, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
ws.close();
|
||||
};
|
||||
}
|
||||
|
||||
function wsEnvoyer(msg) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(msg));
|
||||
}
|
||||
}
|
||||
|
||||
function wsOnMessage(type, callback) {
|
||||
wsCallbacks[type] = callback;
|
||||
}
|
||||
|
||||
// Connexion au demarrage
|
||||
wsConnecter();
|
||||
Reference in New Issue
Block a user