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:
602
frontend/css/style.css
Normal file
602
frontend/css/style.css
Normal file
@@ -0,0 +1,602 @@
|
||||
/* === Variables et Reset === */
|
||||
:root {
|
||||
--primaire: #e91e63;
|
||||
--secondaire: #ffffff;
|
||||
--fond: #1a1a2e;
|
||||
--fond-carte: #16213e;
|
||||
--texte: #ffffff;
|
||||
--texte-secondaire: #a0a0b0;
|
||||
--danger: #f44336;
|
||||
--succes: #4caf50;
|
||||
--rayon: 16px;
|
||||
--ombre: 0 8px 32px rgba(0,0,0,0.3);
|
||||
--transition: 0.3s ease;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-touch-callout: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
background: var(--fond);
|
||||
color: var(--texte);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* === Ecrans === */
|
||||
.ecran {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
|
||||
.ecran.actif {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
/* === Ecran d'accueil === */
|
||||
.accueil-contenu {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.accueil-contenu h1 {
|
||||
font-size: 3.5rem;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primaire);
|
||||
}
|
||||
|
||||
.accueil-instruction {
|
||||
font-size: 1.4rem;
|
||||
color: var(--texte-secondaire);
|
||||
animation: pulse-texte 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-texte {
|
||||
0%, 100% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.accueil-animation {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.cercle-pulse {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: var(--primaire);
|
||||
margin: 0 auto;
|
||||
animation: pulse-cercle 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-cercle {
|
||||
0% { transform: scale(1); opacity: 0.7; }
|
||||
50% { transform: scale(1.2); opacity: 1; }
|
||||
100% { transform: scale(1); opacity: 0.7; }
|
||||
}
|
||||
|
||||
/* Zone admin invisible */
|
||||
.zone-admin {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
/* === Choix du mode === */
|
||||
.grille-modes {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.btn-mode {
|
||||
background: var(--fond-carte);
|
||||
border: 2px solid transparent;
|
||||
border-radius: var(--rayon);
|
||||
padding: 2rem 3rem;
|
||||
color: var(--texte);
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
box-shadow: var(--ombre);
|
||||
}
|
||||
|
||||
.btn-mode:active {
|
||||
transform: scale(0.95);
|
||||
border-color: var(--primaire);
|
||||
}
|
||||
|
||||
.btn-mode .mode-icone {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
/* === Capture / Preview live === */
|
||||
.preview-live {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.preview-live img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.compte-a-rebours {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.compte-a-rebours span {
|
||||
font-size: 12rem;
|
||||
font-weight: 700;
|
||||
color: var(--primaire);
|
||||
animation: pop 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
0% { transform: scale(0.3); opacity: 0; }
|
||||
60% { transform: scale(1.2); }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.flash-blanc {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: #fff;
|
||||
z-index: 20;
|
||||
animation: flash 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.capture-info {
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0,0,0,0.5);
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-radius: 2rem;
|
||||
font-size: 1rem;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* === Preview photo + Filtres === */
|
||||
.preview-photo-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
max-height: 65vh;
|
||||
}
|
||||
|
||||
.preview-photo-container img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: var(--rayon);
|
||||
box-shadow: var(--ombre);
|
||||
}
|
||||
|
||||
.barre-filtres, .barre-overlays {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-filtre, .btn-overlay {
|
||||
background: var(--fond-carte);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 12px;
|
||||
padding: 0.6rem 1.2rem;
|
||||
color: var(--texte);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-filtre.actif, .btn-overlay.actif {
|
||||
border-color: var(--primaire);
|
||||
background: rgba(233, 30, 99, 0.2);
|
||||
}
|
||||
|
||||
.btn-filtre:active, .btn-overlay:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.preview-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* === Partage === */
|
||||
#ecran-partage h2 {
|
||||
margin-top: 2rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.photo-miniature {
|
||||
margin: 1rem 0;
|
||||
max-height: 30vh;
|
||||
}
|
||||
|
||||
.photo-miniature img {
|
||||
max-height: 30vh;
|
||||
border-radius: var(--rayon);
|
||||
box-shadow: var(--ombre);
|
||||
}
|
||||
|
||||
.grille-partage {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.btn-partage {
|
||||
background: var(--fond-carte);
|
||||
border: 2px solid transparent;
|
||||
border-radius: var(--rayon);
|
||||
padding: 1.5rem 2rem;
|
||||
color: var(--texte);
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
box-shadow: var(--ombre);
|
||||
}
|
||||
|
||||
.btn-partage:active {
|
||||
transform: scale(0.95);
|
||||
border-color: var(--primaire);
|
||||
}
|
||||
|
||||
.partage-icone {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.form-email {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
align-items: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.form-email input {
|
||||
background: var(--fond-carte);
|
||||
border: 2px solid var(--texte-secondaire);
|
||||
border-radius: 12px;
|
||||
padding: 0.8rem 1.2rem;
|
||||
color: var(--texte);
|
||||
font-size: 1.1rem;
|
||||
width: 300px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-email input:focus {
|
||||
border-color: var(--primaire);
|
||||
}
|
||||
|
||||
.zone-qr {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.zone-qr img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.statut-partage {
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.statut-partage.succes { background: rgba(76, 175, 80, 0.2); color: var(--succes); }
|
||||
.statut-partage.erreur { background: rgba(244, 67, 54, 0.2); color: var(--danger); }
|
||||
|
||||
.partage-bas {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* === Galerie === */
|
||||
.grille-galerie {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.grille-galerie img {
|
||||
width: 100%;
|
||||
aspect-ratio: 4/3;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.grille-galerie img:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* === Boutons communs === */
|
||||
.btn-action {
|
||||
background: var(--primaire);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 0.8rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
box-shadow: 0 4px 16px rgba(233, 30, 99, 0.3);
|
||||
}
|
||||
|
||||
.btn-action:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn-secondaire {
|
||||
background: transparent;
|
||||
color: var(--texte-secondaire);
|
||||
border: 2px solid var(--texte-secondaire);
|
||||
border-radius: 12px;
|
||||
padding: 0.8rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.btn-secondaire:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn-retour {
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
background: transparent;
|
||||
color: var(--texte-secondaire);
|
||||
border: 2px solid var(--texte-secondaire);
|
||||
border-radius: 12px;
|
||||
padding: 0.6rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-fermer {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--texte);
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--danger);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 0.8rem 2rem;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-danger:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* === Admin === */
|
||||
.admin-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
.admin-contenu {
|
||||
width: 100%;
|
||||
height: calc(100% - 60px);
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-onglets {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
padding: 1rem;
|
||||
min-width: 160px;
|
||||
background: var(--fond-carte);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.onglet {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--texte-secondaire);
|
||||
padding: 0.8rem 1rem;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.onglet.actif {
|
||||
background: var(--primaire);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.admin-panneau {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 2rem;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.admin-panneau.actif {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Toggles */
|
||||
.toggle-groupe {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.toggle input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
width: 50px;
|
||||
height: 28px;
|
||||
background: #555;
|
||||
border-radius: 14px;
|
||||
position: relative;
|
||||
transition: var(--transition);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-slider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 3px; left: 3px;
|
||||
width: 22px; height: 22px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.toggle input:checked + .toggle-slider {
|
||||
background: var(--primaire);
|
||||
}
|
||||
|
||||
.toggle input:checked + .toggle-slider::after {
|
||||
left: 25px;
|
||||
}
|
||||
|
||||
/* Champs admin */
|
||||
.champ {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.champ label {
|
||||
color: var(--texte-secondaire);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.champ input, .champ select {
|
||||
background: var(--fond);
|
||||
border: 2px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem 1rem;
|
||||
color: var(--texte);
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.champ input:focus, .champ select:focus {
|
||||
border-color: var(--primaire);
|
||||
}
|
||||
|
||||
/* === Utilitaires === */
|
||||
.cache {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Scrollbar tactile */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #444;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Responsive tactile */
|
||||
@media (max-width: 800px) {
|
||||
.accueil-contenu h1 { font-size: 2.5rem; }
|
||||
.grille-modes { flex-direction: column; gap: 1rem; }
|
||||
.btn-mode { padding: 1.5rem 2rem; }
|
||||
.grille-partage { flex-direction: column; gap: 1rem; }
|
||||
.admin-contenu { flex-direction: column; }
|
||||
.admin-onglets { flex-direction: row; min-width: unset; overflow-x: auto; }
|
||||
}
|
||||
257
frontend/index.html
Normal file
257
frontend/index.html
Normal file
@@ -0,0 +1,257 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Photobooth</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
|
||||
<!-- Ecran d'accueil -->
|
||||
<section id="ecran-accueil" class="ecran actif">
|
||||
<div class="accueil-contenu">
|
||||
<h1 id="nom-evenement">Photobooth</h1>
|
||||
<p class="accueil-instruction">Touchez pour commencer</p>
|
||||
<div class="accueil-animation">
|
||||
<div class="cercle-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Zone admin : coin bas-droit, appui long 3s -->
|
||||
<div id="zone-admin" class="zone-admin"></div>
|
||||
</section>
|
||||
|
||||
<!-- Choix du mode -->
|
||||
<section id="ecran-mode" class="ecran">
|
||||
<h2>Choisissez votre mode</h2>
|
||||
<div class="grille-modes">
|
||||
<button class="btn-mode" data-mode="simple">
|
||||
<div class="mode-icone">📷</div>
|
||||
<span>Photo</span>
|
||||
</button>
|
||||
<button class="btn-mode" data-mode="multi" id="btn-multi">
|
||||
<div class="mode-icone">🎴</div>
|
||||
<span>Multi-shot</span>
|
||||
</button>
|
||||
<button class="btn-mode" data-mode="gif" id="btn-gif">
|
||||
<div class="mode-icone">🎦</div>
|
||||
<span>GIF</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn-retour" onclick="allerA('accueil')">Retour</button>
|
||||
</section>
|
||||
|
||||
<!-- Compte a rebours + Capture -->
|
||||
<section id="ecran-capture" class="ecran">
|
||||
<div id="preview-live" class="preview-live">
|
||||
<img id="img-preview" src="" alt="Preview">
|
||||
</div>
|
||||
<div id="compte-a-rebours" class="compte-a-rebours cache">
|
||||
<span id="chiffre-car">3</span>
|
||||
</div>
|
||||
<div id="flash-blanc" class="flash-blanc cache"></div>
|
||||
<div class="capture-info">
|
||||
<span id="capture-compteur"></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Preview photo + Filtres -->
|
||||
<section id="ecran-preview" class="ecran">
|
||||
<div class="preview-photo-container">
|
||||
<img id="photo-resultat" src="" alt="Photo">
|
||||
</div>
|
||||
<div id="barre-filtres" class="barre-filtres">
|
||||
<!-- Rempli dynamiquement -->
|
||||
</div>
|
||||
<div id="barre-overlays" class="barre-overlays cache">
|
||||
<!-- Rempli dynamiquement -->
|
||||
</div>
|
||||
<div class="preview-actions">
|
||||
<button class="btn-action" onclick="allerA('partage')">Valider</button>
|
||||
<button class="btn-secondaire" onclick="recommencer()">Reprendre</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Partage -->
|
||||
<section id="ecran-partage" class="ecran">
|
||||
<h2>Partagez votre photo !</h2>
|
||||
<div class="photo-miniature">
|
||||
<img id="photo-partage" src="" alt="Photo">
|
||||
</div>
|
||||
<div class="grille-partage">
|
||||
<button class="btn-partage" id="btn-imprimer" onclick="lancerImpression()">
|
||||
<div class="partage-icone">🖨</div>
|
||||
<span>Imprimer</span>
|
||||
</button>
|
||||
<button class="btn-partage" id="btn-email" onclick="ouvrirEmail()">
|
||||
<div class="partage-icone">📧</div>
|
||||
<span>Email</span>
|
||||
</button>
|
||||
<button class="btn-partage" id="btn-qr" onclick="afficherQR()">
|
||||
<div class="partage-icone">🔒</div>
|
||||
<span>QR Code</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Formulaire email -->
|
||||
<div id="form-email" class="form-email cache">
|
||||
<input type="email" id="input-email" placeholder="votre@email.com" autocomplete="off">
|
||||
<button class="btn-action" onclick="envoyerEmail()">Envoyer</button>
|
||||
<button class="btn-secondaire" onclick="fermerEmail()">Annuler</button>
|
||||
</div>
|
||||
<!-- QR Code -->
|
||||
<div id="zone-qr" class="zone-qr cache">
|
||||
<img id="img-qr" src="" alt="QR Code">
|
||||
</div>
|
||||
<!-- Message statut -->
|
||||
<div id="statut-partage" class="statut-partage cache"></div>
|
||||
<div class="partage-bas">
|
||||
<button class="btn-action" onclick="allerA('accueil')">Terminer</button>
|
||||
<button class="btn-secondaire" onclick="recommencer()">Nouvelle photo</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Galerie -->
|
||||
<section id="ecran-galerie" class="ecran">
|
||||
<h2>Galerie</h2>
|
||||
<div id="grille-galerie" class="grille-galerie">
|
||||
<!-- Rempli dynamiquement -->
|
||||
</div>
|
||||
<button class="btn-retour" onclick="allerA('accueil')">Retour</button>
|
||||
</section>
|
||||
|
||||
<!-- Menu Admin -->
|
||||
<section id="ecran-admin" class="ecran">
|
||||
<div class="admin-header">
|
||||
<h2>Administration</h2>
|
||||
<button class="btn-fermer" onclick="allerA('accueil')">×</button>
|
||||
</div>
|
||||
<div class="admin-contenu">
|
||||
<!-- Onglets -->
|
||||
<div class="admin-onglets">
|
||||
<button class="onglet actif" data-onglet="fonctionnalites">Fonctions</button>
|
||||
<button class="onglet" data-onglet="evenement">Evenement</button>
|
||||
<button class="onglet" data-onglet="camera-admin">Camera</button>
|
||||
<button class="onglet" data-onglet="impression-admin">Impression</button>
|
||||
<button class="onglet" data-onglet="email-admin">Email</button>
|
||||
<button class="onglet" data-onglet="galerie-admin">Galerie</button>
|
||||
<button class="onglet" data-onglet="systeme">Systeme</button>
|
||||
</div>
|
||||
|
||||
<!-- Panneau Fonctionnalites -->
|
||||
<div class="admin-panneau actif" id="panneau-fonctionnalites">
|
||||
<div class="toggle-groupe">
|
||||
<label class="toggle"><input type="checkbox" id="tog-photo-simple" checked><span class="toggle-slider"></span> Photo simple</label>
|
||||
<label class="toggle"><input type="checkbox" id="tog-multi-shot"><span class="toggle-slider"></span> Multi-shot</label>
|
||||
<label class="toggle"><input type="checkbox" id="tog-gif"><span class="toggle-slider"></span> GIF anime</label>
|
||||
<label class="toggle"><input type="checkbox" id="tog-filtres"><span class="toggle-slider"></span> Filtres</label>
|
||||
<label class="toggle"><input type="checkbox" id="tog-overlays"><span class="toggle-slider"></span> Overlays / Cadres</label>
|
||||
<label class="toggle"><input type="checkbox" id="tog-chroma-key"><span class="toggle-slider"></span> Chroma key (fond vert)</label>
|
||||
<label class="toggle"><input type="checkbox" id="tog-impression"><span class="toggle-slider"></span> Impression</label>
|
||||
<label class="toggle"><input type="checkbox" id="tog-email"><span class="toggle-slider"></span> Email</label>
|
||||
<label class="toggle"><input type="checkbox" id="tog-qr-code"><span class="toggle-slider"></span> QR Code</label>
|
||||
<label class="toggle"><input type="checkbox" id="tog-galerie"><span class="toggle-slider"></span> Galerie</label>
|
||||
<label class="toggle"><input type="checkbox" id="tog-compteur"><span class="toggle-slider"></span> Compteur</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Panneau Evenement -->
|
||||
<div class="admin-panneau" id="panneau-evenement">
|
||||
<div class="champ">
|
||||
<label>Nom de l'evenement</label>
|
||||
<input type="text" id="admin-nom-event" placeholder="Mon Evenement">
|
||||
</div>
|
||||
<div class="champ">
|
||||
<label>Couleur primaire</label>
|
||||
<input type="color" id="admin-couleur-primaire" value="#e91e63">
|
||||
</div>
|
||||
<div class="champ">
|
||||
<label>Couleur secondaire</label>
|
||||
<input type="color" id="admin-couleur-secondaire" value="#ffffff">
|
||||
</div>
|
||||
<button class="btn-action" onclick="sauvegarderEvenement()">Sauvegarder</button>
|
||||
</div>
|
||||
|
||||
<!-- Panneau Camera -->
|
||||
<div class="admin-panneau" id="panneau-camera-admin">
|
||||
<div class="champ">
|
||||
<label>Statut</label>
|
||||
<span id="admin-camera-statut">--</span>
|
||||
<button class="btn-secondaire" onclick="reconnecterCamera()">Reconnecter</button>
|
||||
</div>
|
||||
<div class="champ">
|
||||
<label>Compte a rebours (secondes)</label>
|
||||
<input type="number" id="admin-car" min="1" max="10" value="3">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Panneau Impression -->
|
||||
<div class="admin-panneau" id="panneau-impression-admin">
|
||||
<div class="champ">
|
||||
<label>Imprimante</label>
|
||||
<select id="admin-imprimante"><option value="">Chargement...</option></select>
|
||||
</div>
|
||||
<div class="champ">
|
||||
<label>Copies</label>
|
||||
<input type="number" id="admin-copies" min="1" max="5" value="1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Panneau Email -->
|
||||
<div class="admin-panneau" id="panneau-email-admin">
|
||||
<div class="champ">
|
||||
<label>Serveur SMTP</label>
|
||||
<input type="text" id="admin-smtp-host" placeholder="smtp.gmail.com">
|
||||
</div>
|
||||
<div class="champ">
|
||||
<label>Port</label>
|
||||
<input type="number" id="admin-smtp-port" value="587">
|
||||
</div>
|
||||
<div class="champ">
|
||||
<label>Utilisateur</label>
|
||||
<input type="text" id="admin-smtp-user" placeholder="user@gmail.com">
|
||||
</div>
|
||||
<div class="champ">
|
||||
<label>Mot de passe</label>
|
||||
<input type="password" id="admin-smtp-pass">
|
||||
</div>
|
||||
<div class="champ">
|
||||
<label>Expediteur</label>
|
||||
<input type="text" id="admin-expediteur" placeholder="photobooth@event.com">
|
||||
</div>
|
||||
<button class="btn-action" onclick="sauvegarderEmail()">Sauvegarder</button>
|
||||
</div>
|
||||
|
||||
<!-- Panneau Galerie -->
|
||||
<div class="admin-panneau" id="panneau-galerie-admin">
|
||||
<div class="champ">
|
||||
<label>Photos prises : <span id="admin-nb-photos">0</span></label>
|
||||
<label>Exports : <span id="admin-nb-exports">0</span></label>
|
||||
</div>
|
||||
<div class="champ">
|
||||
<label>URL galerie en ligne</label>
|
||||
<input type="text" id="admin-url-galerie" placeholder="https://...">
|
||||
</div>
|
||||
<button class="btn-action" onclick="sauvegarderGalerie()">Sauvegarder URL</button>
|
||||
<button class="btn-danger" onclick="confirmerViderGalerie()">Vider toute la galerie</button>
|
||||
</div>
|
||||
|
||||
<!-- Panneau Systeme -->
|
||||
<div class="admin-panneau" id="panneau-systeme">
|
||||
<button class="btn-danger" onclick="confirmerRedemarrage()">Redemarrer le systeme</button>
|
||||
<button class="btn-danger" onclick="confirmerExtinction()">Eteindre le systeme</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/js/websocket.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
<script src="/js/camera.js"></script>
|
||||
<script src="/js/effects.js"></script>
|
||||
<script src="/js/gallery.js"></script>
|
||||
<script src="/js/share.js"></script>
|
||||
<script src="/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
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