Files
photoBooth/frontend/templates/admin/settings.html
T
admin b2d3bb1833
🚀 Deploy — JH Photomaton / 🔍 Vérification (push) Has been cancelled
🚀 Deploy — JH Photomaton / 🍓 Deploy sur le Pi (push) Has been cancelled
feat: rampe LED progressive en salle sombre + fix setFlashPreset
2026-07-21 00:24:11 +02:00

980 lines
42 KiB
HTML

{% extends "base.html" %}
{% block title %}Réglages bouton — JH Photomaton{% endblock %}
{% block head %}
<style>
/* ── Layout ──────────────────────────────────────────────────────────────── */
.settings-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
max-width: 1100px;
margin: 1.5rem auto;
padding: 0 1rem;
}
@media (max-width: 768px) { .settings-grid { grid-template-columns: 1fr; } }
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
}
.card-title {
font-size: 1.1rem;
font-weight: 700;
color: var(--primary);
margin-bottom: 1.25rem;
display: flex;
align-items: center;
gap: .5rem;
}
/* ── Sliders ─────────────────────────────────────────────────────────────── */
.timing-row {
display: flex;
flex-direction: column;
gap: .4rem;
margin-bottom: 1.25rem;
}
.timing-label {
display: flex;
justify-content: space-between;
align-items: center;
}
.timing-label span:first-child { color: var(--text-muted); font-size: .9rem; }
.timing-value {
font-family: 'Courier New', monospace;
font-size: 1.05rem;
font-weight: 700;
color: var(--primary);
min-width: 5ch;
text-align: right;
}
/* Toggle switch */
.toggle-switch { position: relative; display: inline-block; width: 48px; height: 26px; flex-shrink: 0; }
.toggle-switch input { opacity: 0; width: 0; height: 0; }
.toggle-slider {
position: absolute; inset: 0; cursor: pointer;
background: var(--border); border-radius: 26px; transition: .3s;
}
.toggle-slider::before {
content: ''; position: absolute;
width: 20px; height: 20px; left: 3px; bottom: 3px;
background: #fff; border-radius: 50%; transition: .3s;
}
.toggle-switch input:checked + .toggle-slider { background: var(--primary); }
.toggle-switch input:checked + .toggle-slider::before { transform: translateX(22px); }
input[type=range] {
width: 100%;
accent-color: var(--primary);
height: 6px;
cursor: pointer;
}
.timing-hint { font-size: .78rem; color: var(--text-muted); }
select.field {
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
color: var(--text);
border-radius: 6px;
padding: .45rem .7rem;
font-size: .95rem;
}
.btn-row { display: flex; gap: .75rem; margin-top: 1.5rem; flex-wrap: wrap; }
.btn { padding: .55rem 1.2rem; border: none; border-radius: 8px; cursor: pointer; font-size: .95rem; font-weight: 600; transition: opacity .2s; }
.btn:hover { opacity: .85; }
.btn-primary { background: var(--primary); color: #fff; }
.btn-success { background: #1a7340; color: #fff; }
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
.btn-sm { padding: .35rem .8rem; font-size: .85rem; }
/* ── Testeur ─────────────────────────────────────────────────────────────── */
.tester-area { display: flex; flex-direction: column; align-items: center; gap: 1.25rem; }
#big-btn {
width: 180px;
height: 180px;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #2a6db0, #0d3d6b);
border: 4px solid var(--primary);
box-shadow: 0 0 0 0 rgba(25, 108, 176, .5);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: .4rem;
user-select: none;
touch-action: none;
transition: transform .1s, box-shadow .1s;
color: #fff;
font-size: .9rem;
font-weight: 600;
text-align: center;
}
#big-btn:active, #big-btn.pressed {
transform: scale(.94);
box-shadow: 0 0 0 16px rgba(25, 108, 176, .2);
}
#big-btn .btn-icon { font-size: 2.2rem; pointer-events: none; }
#big-btn .btn-hint { font-size: .75rem; opacity: .7; pointer-events: none; }
/* Anneau de compte pendant multi-clic */
.click-dots { display: flex; gap: .4rem; justify-content: center; }
.click-dot {
width: 12px; height: 12px;
border-radius: 50%;
background: var(--border);
transition: background .15s;
}
.click-dot.active { background: var(--primary); }
.click-dot.max { background: #1a7340; }
/* Résultat */
.result-box {
min-height: 52px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
font-weight: 800;
letter-spacing: .05em;
border-radius: 10px;
padding: .5rem 1.5rem;
width: 100%;
transition: background .3s, color .3s;
background: var(--bg);
color: var(--text-muted);
text-align: center;
}
.result-box.res-click { background: rgba(25,108,176,.2); color: var(--primary); }
.result-box.res-long { background: rgba(224,123,0,.2); color: #e07b00; }
.result-box.res-error { background: rgba(192,0,0,.2); color: #e05050; }
/* Log */
.event-log {
width: 100%;
max-height: 160px;
overflow-y: auto;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: .5rem .75rem;
font-size: .82rem;
color: var(--text-muted);
}
.log-entry {
display: flex;
justify-content: space-between;
padding: .15rem 0;
border-bottom: 1px solid rgba(255,255,255,.04);
animation: fadeIn .3s ease;
}
.log-entry .log-val { color: var(--text); font-weight: 600; }
.log-entry .log-src { font-size: .75rem; opacity: .6; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
/* ── GPIO live ───────────────────────────────────────────────────────────── */
.gpio-badge {
display: inline-flex;
align-items: center;
gap: .4rem;
padding: .3rem .7rem;
border-radius: 20px;
background: var(--bg);
border: 1px solid var(--border);
font-size: .9rem;
font-weight: 600;
}
.gpio-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--border); }
.gpio-dot.live { background: #1a7340; animation: pulse 1s infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .4; } }
/* ── Status banner ───────────────────────────────────────────────────────── */
.status-banner {
max-width: 1100px;
margin: 1rem auto;
padding: 0 1rem;
}
.banner { padding: .65rem 1rem; border-radius: 8px; font-size: .9rem; display: none; }
.banner.show { display: block; }
.banner-ok { background: rgba(26,115,64,.15); border: 1px solid #1a7340; color: #4caf50; }
.banner-err { background: rgba(192,0,0,.12); border: 1px solid #c00; color: #e05050; }
</style>
{% endblock %}
{% block content %}
<div class="status-banner">
<div class="banner banner-ok" id="banner-ok">✅ Réglages appliqués avec succès.</div>
<div class="banner banner-err" id="banner-err">❌ Erreur lors de l'application des réglages.</div>
</div>
<div class="settings-grid">
<!-- ── COLONNE GAUCHE : Timings ─────────────────────────────────────────── -->
<div>
<div class="card">
<div class="card-title">⏱ Timing des clics</div>
<!-- Fenêtre multi-clic -->
<div class="timing-row">
<div class="timing-label">
<span>Fenêtre multi-clic</span>
<span class="timing-value" id="val-dc">{{ button.double_click_ms }} ms</span>
</div>
<input type="range" id="double-click-ms"
min="150" max="800" step="25"
value="{{ button.double_click_ms }}"
oninput="updateSlider('dc', this.value, ' ms')">
<div class="timing-hint">
Délai d'attente entre deux clics. Trop court = double-clic raté. Trop long = lenteur.
</div>
</div>
<!-- Long press -->
<div class="timing-row">
<div class="timing-label">
<span>Durée appui long</span>
<span class="timing-value" id="val-lp">{{ button.long_press_ms }} ms</span>
</div>
<input type="range" id="long-press-ms"
min="500" max="3000" step="100"
value="{{ button.long_press_ms }}"
oninput="updateSlider('lp', this.value, ' ms')">
<div class="timing-hint">
Durée minimale pour déclencher "impression" en maintenant le bouton appuyé.
</div>
</div>
<!-- Anti-rebond -->
<div class="timing-row">
<div class="timing-label">
<span>Anti-rebond (debounce)</span>
<span class="timing-value" id="val-db">{{ button.debounce_ms }} ms</span>
</div>
<input type="range" id="debounce-ms"
min="10" max="200" step="5"
value="{{ button.debounce_ms }}"
oninput="updateSlider('db', this.value, ' ms')">
<div class="timing-hint">
Filtre les faux contacts mécaniques. Augmenter si le bouton "rebondit".
</div>
</div>
<!-- Nombre max de clics -->
<div class="timing-row">
<div class="timing-label">
<span>Nombre max de clics détectés</span>
</div>
<select class="field" id="max-clicks">
{% for n in [1,2,3,4] %}
<option value="{{ n }}" {% if n == button.max_clicks %}selected{% endif %}>
{{ n }} clic{{ 's' if n > 1 else '' }}
</option>
{% endfor %}
</select>
<div class="timing-hint">
Correspond au nombre d'actions mappées (voir page Actions).
</div>
</div>
<!-- Impression long press -->
<div class="timing-row">
<div class="timing-label">
<span>Impression via appui long</span>
<select class="field" id="print-enabled" style="width:auto">
<option value="true" {% if button.print_enabled %}selected{% endif %}>Activée</option>
<option value="false" {% if not button.print_enabled %}selected{% endif %}>Désactivée</option>
</select>
</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" onclick="applyTimings(false)">
▶ Appliquer (sans redémarrage)
</button>
<button class="btn btn-success" onclick="applyTimings(true)">
💾 Sauvegarder
</button>
</div>
<p style="font-size:.78rem; color:var(--text-muted); margin-top:.75rem;">
<strong>Appliquer</strong> met les réglages en ligne immédiatement.<br>
<strong>Sauvegarder</strong> les persiste dans <code>settings.yaml</code> pour les redémarrages.
</p>
</div>
<!-- Contrôles interface photobooth-app -->
<div class="card" style="margin-top:1rem">
<div class="card-title">🎛 Interface photobooth-app</div>
<div style="display:flex; align-items:center; justify-content:space-between; gap:1rem; padding:.5rem 0; border-bottom:1px solid var(--border);">
<div>
<div style="font-size:.95rem; font-weight:600;">Bouton Supprimer (review)</div>
<div style="font-size:.78rem; color:var(--text-muted); margin-top:.2rem;">
Affiche ou cache le bouton 🗑 sur l'écran de validation après chaque capture.<br>
Effectif immédiatement (modifie <code>userdata/private.css</code>).
</div>
</div>
<label class="toggle-switch" title="Bouton Supprimer visible dans photobooth-app">
<input type="checkbox" id="toggle-delete-btn"
{% if config.photobooth.show_delete_button %}checked{% endif %}
onchange="setDeleteButton(this.checked)">
<span class="toggle-slider"></span>
</label>
</div>
<div style="display:flex; align-items:center; gap:.75rem; margin-top:.75rem; font-size:.82rem; color:var(--text-muted);" id="delete-btn-status">
{% if config.photobooth.show_delete_button %}
✅ Bouton Supprimer actuellement <strong style="color:#4caf50">visible</strong>
{% else %}
🚫 Bouton Supprimer actuellement <strong style="color:#e07b00">caché</strong>
{% endif %}
</div>
</div>
<!-- Actions système -->
<div class="card" style="margin-top:1rem">
<div class="card-title">🖥 Affichage HDMI</div>
<p style="font-size:.85rem; color:var(--text-muted); margin-bottom:1rem;">
Rafraîchit le Chromium en kiosque connecté à l'écran HDMI.
Équivalent à appuyer sur <code>F5</code> sur le Pi.
</p>
<button class="btn btn-outline" onclick="refreshScreen()" id="btn-refresh-screen">
🔄 Rafraîchir l'écran (F5)
</button>
<span id="refresh-status" style="font-size:.82rem; color:var(--text-muted); margin-left:.75rem;"></span>
</div>
<!-- Luminosité LED -->
<div class="card" style="margin-top:1rem">
<div class="card-title">&#x1F4A1; Luminosit&eacute; LED</div>
<div style="display:grid; gap:1rem;">
<div>
<label style="font-size:.82rem; color:var(--text-muted)">
Luminosit&eacute; ambiante : <strong id="brightness-val">180</strong> / 255
</label>
<input type="range" id="brightness-slider" min="0" max="255" step="5" value="180"
oninput="document.getElementById('brightness-val').textContent=this.value"
style="width:100%">
<div style="font-size:.72rem; color:var(--text-muted)">
Appliqu&eacute;e en permanence (idle, countdown, captured…). Le flash peut d&eacute;passer cette valeur.
</div>
</div>
<div style="display:flex; gap:.5rem; align-items:center; flex-wrap:wrap;">
<button class="btn btn-ghost btn-sm" onclick="previewBrightness()">&#x1F441; Aperçu</button>
<button class="btn btn-primary btn-sm" onclick="saveBrightness()">&#x1F4BE; Sauvegarder</button>
<span id="brightness-status" style="font-size:.82rem; color:var(--text-muted)"></span>
</div>
<div style="border-top:1px solid var(--border); padding-top:1rem;">
<div style="display:flex; align-items:center; justify-content:space-between;">
<div>
<div style="font-weight:600; font-size:.9rem;">&#x1F31E; Luminosit&eacute; flash automatique</div>
<div style="font-size:.78rem; color:var(--text-muted); margin-top:.2rem;">
Analyse une frame de la cam&eacute;ra avant la prise pour adapter le flash.<br>
Salle sombre → flash plein (255) &nbsp;|&nbsp; Salle claire → flash r&eacute;duit (80+).
</div>
</div>
<label class="toggle" style="flex-shrink:0; margin-left:1rem;">
<input type="checkbox" id="toggle-auto-brightness"
onchange="setAutoBrightness(this.checked)">
<span class="slider"></span>
</label>
</div>
<p id="auto-brightness-status" style="font-size:.78rem; color:var(--text-muted); margin-top:.5rem;">
&#x23F3; Chargement…
</p>
<div style="display:flex; gap:.5rem; align-items:center; margin-top:.5rem; flex-wrap:wrap;">
<button class="btn btn-ghost btn-sm" onclick="measureAmbient()" id="btn-measure-lux">
&#x1F4F7; Tester la mesure
</button>
<span id="lux-status" style="font-size:.8rem; color:var(--text-muted);"></span>
</div>
<div id="lux-info" style="font-size:.78rem; margin-top:.4rem; color:var(--text-muted); display:none;">
Luminosité mesurée : <strong id="lux-val"></strong> / 255 &nbsp;&nbsp;
<span id="lux-desc"></span>
</div>
</div>
</div>
</div>
<!-- Flash LED -->
<div class="card" style="margin-top:1rem">
<div class="card-title">&#x26A1; Flash photo (LED)</div>
<p style="font-size:.82rem; color:var(--text-muted); margin-bottom:1rem;">
Les LEDs WS2812b ont une diode bleue plus efficace que la rouge.<br>
<code>[255,255,255]</code> produit un rendu &agrave; dominante bleue sur les photos.<br>
R&eacute;duisez le bleu et augmentez le rouge pour un blanc plus naturel.
</p>
<div style="display:grid; gap:.75rem;">
<div>
<label style="font-size:.82rem; color:var(--text-muted)">Couleur flash (R G B)</label>
<div style="display:flex; gap:.5rem; align-items:center; margin-top:.25rem; flex-wrap:wrap;">
<input type="number" id="flash-r" min="0" max="255" value="255"
style="width:70px" class="form-control" placeholder="R">
<input type="number" id="flash-g" min="0" max="255" value="200"
style="width:70px" class="form-control" placeholder="G">
<input type="number" id="flash-b" min="0" max="255" value="80"
style="width:70px" class="form-control" placeholder="B">
<div id="flash-preview" style="width:36px;height:36px;border-radius:50%;border:2px solid var(--border);background:rgb(255,200,80)"></div>
<input type="color" id="flash-colorpicker" value="#ffc850"
title="Aide visuelle (convertit en RGB approx)"
style="width:36px;height:36px;cursor:pointer;border:none;background:none"
onchange="colorPickerToRgb(this.value)">
</div>
</div>
<div>
<label style="font-size:.82rem; color:var(--text-muted)">
Dur&eacute;e du flash : <strong id="flash-dur-val">0.30</strong> s
</label>
<input type="range" id="flash-duration" min="0.05" max="1.0" step="0.05" value="0.30"
oninput="document.getElementById('flash-dur-val').textContent=parseFloat(this.value).toFixed(2)"
style="width:100%">
<div style="font-size:.72rem;color:var(--text-muted)">0.05 s (bref) &mdash; 1.0 s (long)</div>
</div>
<div>
<label style="font-size:.82rem; color:var(--text-muted)">Nombre de flashs</label>
<select id="flash-count" class="form-control" style="width:100px; margin-top:.25rem">
<option value="1">1</option>
<option value="2" selected>2</option>
<option value="3">3</option>
</select>
</div>
<div style="display:flex; gap:.5rem; flex-wrap:wrap;">
<button class="btn btn-ghost btn-sm" onclick="previewFlash()">&#x1F4A1; Tester le flash</button>
<button class="btn btn-primary btn-sm" onclick="saveFlash()">&#x1F4BE; Sauvegarder</button>
</div>
<div style="display:flex; gap:.4rem; flex-wrap:wrap; margin-top:.25rem;">
<span style="font-size:.75rem; color:var(--text-muted); align-self:center;">Presets :</span>
<button class="btn btn-ghost btn-sm" onclick="setFlashPreset(255,255,255,0.15,2)">Blanc pur (froid)</button>
<button class="btn btn-ghost btn-sm" onclick="setFlashPreset(255,200,80,0.30,2)">Blanc chaud &#x2605;</button>
<button class="btn btn-ghost btn-sm" onclick="setFlashPreset(255,220,120,0.25,2)">Blanc neutre</button>
<button class="btn btn-ghost btn-sm" onclick="setFlashPreset(255,180,40,0.35,2)">Ambre</button>
</div>
<div style="margin-top:1rem; padding-top:.75rem; border-top:1px solid var(--border);">
<label style="font-size:.82rem; color:var(--text-muted)">⏱ Déclencher le flash
<strong id="pre-flash-val">0.5</strong> s avant la fin du countdown</label>
<input type="range" id="pre-flash-advance" min="0" max="3" step="0.1" value="0.5"
oninput="document.getElementById('pre-flash-val').textContent=parseFloat(this.value).toFixed(1)"
style="width:100%; margin-top:.4rem">
<div style="display:flex; justify-content:space-between; font-size:.72rem; color:var(--text-muted)">
<span>0 s (au moment de la capture)</span><span>3 s (très en avance)</span>
</div>
<button class="btn btn-primary btn-sm" style="margin-top:.5rem" onclick="savePreFlash()">&#x1F4BE; Sauvegarder</button>
<span id="pre-flash-status" style="font-size:.82rem; color:var(--text-muted); margin-left:.5rem"></span>
</div>
</div>
</div>
<!-- Valeurs recommandées -->
<div class="card" style="margin-top:1rem">
<div class="card-title">📋 Préréglages</div>
<div style="display:flex; flex-wrap:wrap; gap:.5rem;">
<button class="btn btn-outline btn-sm" onclick="applyPreset(350, 1200, 30, 4)">Rapide</button>
<button class="btn btn-outline btn-sm" onclick="applyPreset(400, 1500, 50, 4)">Normal (défaut)</button>
<button class="btn btn-outline btn-sm" onclick="applyPreset(500, 2000, 80, 4)">Lent / senior</button>
<button class="btn btn-outline btn-sm" onclick="applyPreset(600, 2500, 100, 4)">Très lent</button>
</div>
<p style="font-size:.78rem; color:var(--text-muted); margin-top:.75rem;">
Les préréglages mettent à jour les sliders uniquement.
Cliquez ensuite sur Appliquer ou Sauvegarder.
</p>
</div>
</div>
<!-- ── COLONNE DROITE : Testeur ─────────────────────────────────────────── -->
<div>
<div class="card">
<div class="card-title">🎯 Testeur de timings</div>
<p style="font-size:.85rem; color:var(--text-muted); margin-bottom:1rem;">
Simulation <strong>locale</strong> — utilise les valeurs des sliders en temps réel,
même avant de sauvegarder. Aucune action GPIO déclenchée.
</p>
<div class="tester-area">
<!-- Anneau de clics -->
<div class="click-dots" id="click-dots">
<div class="click-dot" id="dot-1"></div>
<div class="click-dot" id="dot-2"></div>
<div class="click-dot" id="dot-3"></div>
<div class="click-dot" id="dot-4"></div>
</div>
<!-- Grand bouton -->
<div id="big-btn"
onmousedown="btnDown(event)"
onmouseup="btnUp(event)"
onmouseleave="btnUp(event)"
ontouchstart="btnDown(event)"
ontouchend="btnUp(event)">
<span class="btn-icon">👆</span>
<span>Appuyer ici</span>
<span class="btn-hint">clic / multi-clic / maintenir</span>
</div>
<!-- Résultat -->
<div class="result-box" id="result-box">En attente…</div>
<!-- Log local -->
<div class="event-log" id="tester-log">
<div style="opacity:.4; text-align:center; padding:.5rem 0">Les événements apparaîtront ici</div>
</div>
</div>
</div>
<!-- GPIO réel via WebSocket -->
<div class="card" style="margin-top:1rem">
<div class="card-title">
🔌 Bouton GPIO réel
<div class="gpio-badge" style="margin-left:auto">
<div class="gpio-dot" id="gpio-dot"></div>
<span id="gpio-label">En attente…</span>
</div>
</div>
<p style="font-size:.85rem; color:var(--text-muted); margin-bottom:.75rem;">
Événements reçus du bouton physique (GPIO {{ button.pin }}) via WebSocket.
</p>
<div class="event-log" id="gpio-log">
<div style="opacity:.4; text-align:center; padding:.5rem 0">Appuyer sur le bouton physique…</div>
</div>
<div style="margin-top:.75rem; display:flex; gap:.5rem; flex-wrap:wrap;">
<button class="btn btn-outline btn-sm" onclick="simulateGPIO(1)">Sim. 1 clic</button>
<button class="btn btn-outline btn-sm" onclick="simulateGPIO(2)">Sim. 2 clics</button>
<button class="btn btn-outline btn-sm" onclick="simulateGPIO(3)">Sim. 3 clics</button>
<button class="btn btn-outline btn-sm" onclick="simulateGPIO(4)">Sim. 4 clics</button>
<button class="btn btn-outline btn-sm" onclick="simulateGPIO(0)">Sim. Long press</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// ════════════════════════════════════════════════════════════════════════════
// Sliders & valeurs
// ════════════════════════════════════════════════════════════════════════════
function updateSlider(key, val, unit) {
document.getElementById('val-' + key).textContent = val + unit;
}
function getTimings() {
return {
doubleClickMs: parseInt(document.getElementById('double-click-ms').value),
longPressMs: parseInt(document.getElementById('long-press-ms').value),
debounceMs: parseInt(document.getElementById('debounce-ms').value),
maxClicks: parseInt(document.getElementById('max-clicks').value),
};
}
function applyPreset(dc, lp, db, mc) {
document.getElementById('double-click-ms').value = dc;
document.getElementById('long-press-ms').value = lp;
document.getElementById('debounce-ms').value = db;
document.getElementById('max-clicks').value = mc;
updateSlider('dc', dc, ' ms');
updateSlider('lp', lp, ' ms');
updateSlider('db', db, ' ms');
showDots(0);
}
// ════════════════════════════════════════════════════════════════════════════
// Apply / Save via API
// ════════════════════════════════════════════════════════════════════════════
async function applyTimings(save) {
const t = getTimings();
const printEnabled = document.getElementById('print-enabled').value === 'true';
try {
const r = await api('PUT', '/api/system/button/config', {
double_click_ms: t.doubleClickMs,
long_press_ms: t.longPressMs,
debounce_ms: t.debounceMs,
max_clicks: t.maxClicks,
print_enabled: printEnabled,
save: save,
});
document.getElementById('banner-ok').className = 'banner banner-ok show';
document.getElementById('banner-err').className = 'banner banner-err';
setTimeout(() => {
document.getElementById('banner-ok').className = 'banner banner-ok';
}, 3000);
showToast(save ? '✅ Réglages sauvegardés dans settings.yaml' : '▶ Timings appliqués (non sauvegardés)', 'success');
} catch(e) {
document.getElementById('banner-err').className = 'banner banner-err show';
document.getElementById('banner-ok').className = 'banner banner-ok';
showToast('Erreur : ' + e.message, 'error');
}
}
// ════════════════════════════════════════════════════════════════════════════
// Testeur de timings (simulation locale — AUCUN GPIO)
// ════════════════════════════════════════════════════════════════════════════
let testerClickCount = 0;
let testerClickTimer = null;
let testerLongTimer = null;
let testerLongFired = false;
function btnDown(e) {
e.preventDefault();
document.getElementById('big-btn').classList.add('pressed');
testerLongFired = false;
if (testerLongTimer) clearTimeout(testerLongTimer);
const { longPressMs } = getTimings();
testerLongTimer = setTimeout(() => {
testerLongFired = true;
if (testerClickTimer) clearTimeout(testerClickTimer);
testerClickCount = 0;
showDots(0);
showTesterResult('LONG PRESS', 'long');
addLog('tester-log', 'Long press', 'testeur');
}, longPressMs);
}
function btnUp(e) {
e.preventDefault();
document.getElementById('big-btn').classList.remove('pressed');
if (testerLongTimer) clearTimeout(testerLongTimer);
if (testerLongFired) return;
const { doubleClickMs, maxClicks } = getTimings();
testerClickCount++;
showDots(testerClickCount);
if (testerClickTimer) clearTimeout(testerClickTimer);
const delay = testerClickCount >= maxClicks ? 50 : doubleClickMs;
testerClickTimer = setTimeout(() => {
const n = testerClickCount;
testerClickCount = 0;
testerClickTimer = null;
showDots(0);
const label = n === 1 ? '1 CLIC' : `${n} CLICS`;
showTesterResult(label, 'click');
addLog('tester-log', label, 'testeur');
}, delay);
}
function showDots(count) {
const max = parseInt(document.getElementById('max-clicks').value);
for (let i = 1; i <= 4; i++) {
const d = document.getElementById('dot-' + i);
if (i > max) {
d.className = 'click-dot';
d.style.opacity = '.2';
} else {
d.style.opacity = '1';
d.className = 'click-dot' + (i <= count ? (count >= max ? ' max' : ' active') : '');
}
}
}
function showTesterResult(label, type) {
const box = document.getElementById('result-box');
box.textContent = label;
box.className = 'result-box res-' + (type === 'long' ? 'long' : type === 'click' ? 'click' : 'error');
setTimeout(() => { box.className = 'result-box'; }, 2500);
}
// ════════════════════════════════════════════════════════════════════════════
// Simulation GPIO serveur
// ════════════════════════════════════════════════════════════════════════════
async function simulateGPIO(clicks) {
try {
await api('POST', `/api/system/button/simulate?clicks=${clicks}`);
showToast(`Simulation envoyée : ${clicks === 0 ? 'long press' : clicks + ' clic(s)'}`, 'info');
} catch(e) {
showToast('Erreur simulation : ' + e.message, 'error');
}
}
// ════════════════════════════════════════════════════════════════════════════
// WebSocket — écoute du bouton GPIO réel
// ════════════════════════════════════════════════════════════════════════════
function onWsMessage(msg) {
if (msg.type !== 'button_event') return;
const dot = document.getElementById('gpio-dot');
const label = document.getElementById('gpio-label');
dot.classList.add('live');
setTimeout(() => dot.classList.remove('live'), 1500);
let text;
if (msg.clicks === 0) {
text = 'LONG PRESS → Impression';
label.textContent = '🖨 Long press';
} else {
const clicks = msg.clicks;
text = `${clicks} CLIC${clicks > 1 ? 'S' : ''}${msg.action || ''}`;
label.textContent = `${clicks} clic${clicks > 1 ? 's' : ''}`;
}
addLog('gpio-log', text, 'GPIO');
}
// ════════════════════════════════════════════════════════════════════════════
// Helpers
// ════════════════════════════════════════════════════════════════════════════
function addLog(targetId, value, source) {
const log = document.getElementById(targetId);
// Vider le placeholder si présent
if (log.querySelector('[style*="opacity"]')) log.innerHTML = '';
const entry = document.createElement('div');
entry.className = 'log-entry';
const now = new Date().toLocaleTimeString('fr-FR');
entry.innerHTML = `
<span class="log-val">${value}</span>
<span class="log-src">[${source}] ${now}</span>
`;
log.insertBefore(entry, log.firstChild);
// Garder 20 lignes max
while (log.children.length > 20) log.removeChild(log.lastChild);
}
// ════════════════════════════════════════════════════════════════════════════
// Bouton Supprimer photobooth-app
// ════════════════════════════════════════════════════════════════════════════
async function setDeleteButton(visible) {
try {
const r = await api('POST', `/api/system/ui/delete-button?visible=${visible}`);
const status = document.getElementById('delete-btn-status');
if (r.ok) {
status.innerHTML = visible
? '✅ Bouton Supprimer actuellement <strong style="color:#4caf50">visible</strong>'
: '🚫 Bouton Supprimer actuellement <strong style="color:#e07b00">caché</strong>';
showToast(visible ? '✅ Bouton Supprimer activé' : '🚫 Bouton Supprimer désactivé', 'success');
} else {
showToast('❌ Erreur écriture private.css', 'error');
// Remettre le toggle dans l'état précédent
document.getElementById('toggle-delete-btn').checked = !visible;
}
} catch(e) {
showToast('Erreur : ' + e.message, 'error');
document.getElementById('toggle-delete-btn').checked = !visible;
}
}
// ════════════════════════════════════════════════════════════════════════════
// Refresh écran HDMI
// ════════════════════════════════════════════════════════════════════════════
async function refreshScreen() {
const btn = document.getElementById('btn-refresh-screen');
const status = document.getElementById('refresh-status');
btn.disabled = true;
status.textContent = 'Envoi…';
try {
const r = await api('POST', '/api/system/screen/refresh');
status.style.color = '#4caf50';
status.textContent = r.ok ? `✅ OK (${r.method})` : `⚠ ${r.error}`;
showToast('Écran rafraîchi', 'success');
} catch(e) {
status.style.color = '#e05050';
status.textContent = '❌ Erreur : ' + e.message;
showToast('Erreur refresh : ' + e.message, 'error');
} finally {
btn.disabled = false;
setTimeout(() => { status.textContent = ''; }, 4000);
}
}
// ════════════════════════════════════════════════════════════════════════════
// Luminosité LED
// ════════════════════════════════════════════════════════════════════════════
async function loadBrightnessConfig() {
try {
const d = await api('GET', '/api/leds/brightness');
document.getElementById('brightness-slider').value = d.brightness ?? 180;
document.getElementById('brightness-val').textContent = d.brightness ?? 180;
const auto = d.auto_brightness ?? false;
document.getElementById('toggle-auto-brightness').checked = auto;
document.getElementById('auto-brightness-status').textContent = auto
? '✅ Auto-flash actif — le flash s\'adapte à l\'ambiance'
: '⏸ Auto-flash désactivé — luminosité fixe';
} catch(e) {
console.warn('brightness config:', e);
}
}
async function previewBrightness() {
const val = parseInt(document.getElementById('brightness-slider').value);
document.getElementById('brightness-val').textContent = val;
try {
await api('PUT', `/api/leds/brightness?value=${val}&save=false`);
showToast(`👁 Aperçu luminosité : ${val}`, 'info');
} catch(e) {
showToast('Erreur aperçu : ' + e.message, 'error');
}
}
async function saveBrightness() {
const val = parseInt(document.getElementById('brightness-slider').value);
try {
await api('PUT', `/api/leds/brightness?value=${val}`);
showToast(`✅ Luminosité ${val} sauvegardée`, 'success');
} catch(e) {
showToast('Erreur : ' + e.message, 'error');
}
}
async function previewFlash() {
try {
await api('POST', '/api/leds/effect?effect=capture');
showToast('💡 Flash déclenché', 'info');
} catch(e) {
showToast('Erreur flash : ' + e.message, 'error');
}
}
function setFlashPreset(r, g, b, duration, flashes) {
document.getElementById('flash-r').value = r;
document.getElementById('flash-g').value = g;
document.getElementById('flash-b').value = b;
document.getElementById('flash-duration').value = duration;
document.getElementById('flash-dur-val').textContent = duration.toFixed(2);
document.getElementById('flash-count').value = flashes;
document.getElementById('flash-preview').style.background = `rgb(${r},${g},${b})`;
const hex = '#' + [r,g,b].map(v => v.toString(16).padStart(2,'0')).join('');
document.getElementById('flash-colorpicker').value = hex;
}
async function saveFlash() {
const r = parseInt(document.getElementById('flash-r').value) || 255;
const g = parseInt(document.getElementById('flash-g').value) || 200;
const b = parseInt(document.getElementById('flash-b').value) || 80;
const duration = parseFloat(document.getElementById('flash-duration').value) || 0.3;
const flashes = parseInt(document.getElementById('flash-count').value) || 2;
try {
await api('PUT', `/api/leds/effect/capture?r=${r}&g=${g}&b=${b}&flash_duration=${duration}&flashes=${flashes}`);
showToast('✅ Flash sauvegardé', 'success');
} catch(e) {
showToast('Erreur : ' + e.message, 'error');
}
}
function colorPickerToRgb(hex) {
const r = parseInt(hex.slice(1,3), 16);
const g = parseInt(hex.slice(3,5), 16);
const b = parseInt(hex.slice(5,7), 16);
document.getElementById('flash-r').value = r;
document.getElementById('flash-g').value = g;
document.getElementById('flash-b').value = b;
document.getElementById('flash-preview').style.background = `rgb(${r},${g},${b})`;
}
async function loadFlashConfig() {
try {
const effects = await api('GET', '/api/leds/effects');
const cap = effects.capture;
if (!cap) return;
if (cap.color) {
document.getElementById('flash-r').value = cap.color[0] ?? 255;
document.getElementById('flash-g').value = cap.color[1] ?? 200;
document.getElementById('flash-b').value = cap.color[2] ?? 80;
document.getElementById('flash-preview').style.background =
`rgb(${cap.color[0]},${cap.color[1]},${cap.color[2]})`;
}
if (cap.flash_duration !== undefined) {
document.getElementById('flash-duration').value = cap.flash_duration;
document.getElementById('flash-dur-val').textContent = cap.flash_duration.toFixed(2);
}
if (cap.flashes !== undefined) {
document.getElementById('flash-count').value = cap.flashes;
}
} catch(e) { console.warn('loadFlashConfig:', e); }
// Charger le timing pre-flash
try {
const t = await api('GET', '/api/leds/flash-timing');
const v = t.pre_flash_advance ?? 0.5;
document.getElementById('pre-flash-advance').value = v;
document.getElementById('pre-flash-val').textContent = v.toFixed(1);
} catch(e) { console.warn('loadFlashTiming:', e); }
}
async function savePreFlash() {
const v = parseFloat(document.getElementById('pre-flash-advance').value);
const status = document.getElementById('pre-flash-status');
try {
await api('PUT', `/api/leds/flash-timing?pre_flash_advance=${v}`);
status.textContent = `✅ Sauvegardé (${v.toFixed(1)} s)`;
setTimeout(() => status.textContent = '', 3000);
} catch(e) {
status.textContent = '❌ Erreur';
}
}
async function measureAmbient() {
const btn = document.getElementById('btn-measure-lux');
const status = document.getElementById('lux-status');
const info = document.getElementById('lux-info');
btn.disabled = true;
status.textContent = '⏳ Mesure en cours…';
status.style.color = 'var(--text-muted)';
try {
const r = await api('POST', '/api/leds/measure-ambient');
if (r.ok) {
document.getElementById('lux-val').textContent = r.ambient_lux;
document.getElementById('lux-desc').textContent = r.description;
info.style.display = '';
status.textContent = '✅ Mesure réussie';
status.style.color = '#4caf50';
showToast(`✅ Luminosité : ${r.ambient_lux}${r.description}`, 'success');
} else {
status.textContent = '❌ ' + (r.error || 'Erreur inconnue');
status.style.color = '#e05050';
showToast('❌ Mesure impossible : ' + r.error, 'error');
}
} catch(e) {
status.textContent = '❌ Liveview inaccessible';
status.style.color = '#e05050';
showToast('❌ Liveview inaccessible (endpoint 404). Vérifiez que photobooth-app est démarré.', 'error');
} finally {
btn.disabled = false;
setTimeout(() => { status.textContent = ''; }, 6000);
}
}
async function setAutoBrightness(enabled) {
try {
await api('PUT', `/api/leds/auto-brightness?enabled=${enabled}`);
document.getElementById('auto-brightness-status').textContent = enabled
? '✅ Auto-flash actif — le flash s\'adapte à l\'ambiance'
: '⏸ Auto-flash désactivé — luminosité fixe';
showToast(enabled ? '✅ Auto-flash activé' : '⏸ Auto-flash désactivé', 'success');
} catch(e) {
showToast('Erreur : ' + e.message, 'error');
document.getElementById('toggle-auto-brightness').checked = !enabled;
}
}
// ── Init ──────────────────────────────────────────────────────────────────────
loadBrightnessConfig();
loadFlashConfig();
</script>
{% endblock %}