From 5e8cff7951ea40ffeba777fa3988b1b0def5b7fb Mon Sep 17 00:00:00 2001 From: Jules Date: Sun, 20 Sep 2026 10:22:00 +0200 Subject: [PATCH] Mini menu client + luminosite impression + endpoints systeme - Menu client coin haut-gauche : WiFi, luminosite, relancer, redemarrer, eteindre - Popup WiFi : scan, statut, connexion (reutilise les endpoints existants) - Popup luminosite : slider -50% a +50%, ajuste la luminosite avant impression (ImageEnhance.Brightness dans _preparer_image) - Endpoints systeme : /api/systeme/eteindre, redemarrer, redemarrer-app - Endpoint exposition Canon : /api/camera/exposition (GET choix + POST valeur) Co-Authored-By: Claude Opus 4.6 --- backend/main.py | 71 ++++++++++++++++++++++++++++- backend/printer.py | 7 +++ frontend/css/style.css | 101 +++++++++++++++++++++++++++++++++++++++++ frontend/index.html | 36 ++++++++++++++- frontend/js/app.js | 85 ++++++++++++++++++++++++++++++++++ 5 files changed, 297 insertions(+), 3 deletions(-) diff --git a/backend/main.py b/backend/main.py index c7f56b3..9b20b41 100644 --- a/backend/main.py +++ b/backend/main.py @@ -495,10 +495,30 @@ async def _reconnecter_dslr_avec_reset(echecs: int): def _appliquer_config_camera(): - """Applique les paramètres caméra persistants (flash, etc.) après connexion.""" + """Applique les paramètres caméra persistants (flash, exposition) après connexion.""" cfg = charger_config() flash_integre = cfg.get("camera", {}).get("flash_integre", True) camera.configurer_flash(flash_integre) + ev = cfg.get("camera", {}).get("exposure_compensation") + if ev is not None: + _set_canon_config("exposurecompensation", str(ev)) + + +def _set_canon_config(nom: str, valeur): + """Applique un réglage gphoto2 sur le Canon.""" + if camera.mode != "gphoto2" or not camera.connectee: + return False + try: + with camera._gp_lock: + cfg = camera.camera.get_config() + w = cfg.get_child_by_name(nom) + w.set_value(valeur) + camera.camera.set_config(cfg) + log.info(f"Canon config: {nom} = {valeur}") + return True + except Exception as e: + log.warning(f"Canon config {nom} échoué: {e}") + return False async def _watchdog_systemd(): @@ -1260,6 +1280,55 @@ async def api_camera_reconnecter(body: dict = {}): return {"connectee": ok, "mode": camera.mode} +@app.post("/api/camera/exposition") +async def api_camera_exposition(body: dict): + """Ajuste la compensation d'exposition Canon (-3 à +3 EV).""" + valeur = body.get("valeur", "0") + ok = _set_canon_config("exposurecompensation", str(valeur)) + if ok: + mettre_a_jour_config({"camera": {"exposure_compensation": str(valeur)}}) + return {"succes": ok, "valeur": valeur} + + +@app.get("/api/camera/exposition") +async def api_camera_exposition_get(): + """Retourne les valeurs possibles et la valeur actuelle d'exposition.""" + result = {"valeur": "0", "choix": []} + if camera.mode == "gphoto2" and camera.connectee: + try: + with camera._gp_lock: + cfg = camera.camera.get_config() + w = cfg.get_child_by_name("exposurecompensation") + result["valeur"] = w.get_value() + result["choix"] = [w.get_choice(i) for i in range(w.count_choices())] + except Exception as e: + result["erreur"] = str(e) + return result + + +# --- API Système (menu client) --- + +@app.post("/api/systeme/eteindre") +async def api_eteindre(): + import subprocess + subprocess.Popen(["sudo", "shutdown", "-h", "now"]) + return {"succes": True, "message": "Extinction en cours..."} + + +@app.post("/api/systeme/redemarrer") +async def api_redemarrer(): + import subprocess + subprocess.Popen(["sudo", "reboot"]) + return {"succes": True, "message": "Redémarrage en cours..."} + + +@app.post("/api/systeme/redemarrer-app") +async def api_redemarrer_app(): + import subprocess, signal + os.kill(os.getpid(), signal.SIGTERM) + return {"succes": True} + + # --- API Effets --- @app.get("/api/filtres") diff --git a/backend/printer.py b/backend/printer.py index e3ec332..f76b901 100644 --- a/backend/printer.py +++ b/backend/printer.py @@ -111,6 +111,13 @@ def _preparer_image(chemin: Path, largeur: int, hauteur: int, if config_imp.get("rotation_180", False) and not skip_rotate: img = img.rotate(180) + luminosite = config_imp.get("luminosite_impression", 0) + if luminosite != 0: + from PIL import ImageEnhance + facteur = 1.0 + luminosite / 100.0 + img = ImageEnhance.Brightness(img).enhance(facteur) + log.debug(f"Luminosité impression ajustée : {luminosite}% (facteur {facteur:.2f})") + tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) img.save(tmp.name, "JPEG", quality=95, dpi=(300, 300)) log.debug(f"Image préparée : {orientation} {largeur}x{hauteur}px → {tmp.name}") diff --git a/frontend/css/style.css b/frontend/css/style.css index 4940ef2..d119c49 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -157,6 +157,107 @@ html, body { color: rgba(255,255,255,0.4); } +/* Mini menu client */ +.btn-menu-client { + position: absolute; + top: 12px; + left: 12px; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.3rem; + color: rgba(255,255,255,0.2); + cursor: pointer; + border-radius: 50%; + transition: color 0.3s; + z-index: 60; +} +.btn-menu-client:active { + color: rgba(255,255,255,0.5); +} +.menu-client { + position: absolute; + top: 60px; + left: 12px; + background: rgba(0,0,0,0.92); + border-radius: 16px; + padding: 16px; + z-index: 200; + min-width: 220px; + display: flex; + flex-direction: column; + gap: 8px; +} +.menu-client button { + display: block; + width: 100%; + padding: 14px 16px; + font-size: 1rem; + text-align: left; + background: rgba(255,255,255,0.08); + color: #fff; + border: none; + border-radius: 10px; + cursor: pointer; +} +.menu-client button:active { + background: rgba(255,255,255,0.2); +} +.menu-client-titre { + font-size: .85rem; + font-weight: 600; + color: rgba(255,255,255,0.5); + text-transform: uppercase; + letter-spacing: 1px; + padding: 0 4px 8px; +} +.popup-wifi { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.85); + z-index: 300; + display: flex; + align-items: center; + justify-content: center; +} +.popup-wifi-inner { + background: rgba(30,30,30,0.98); + border-radius: 20px; + padding: 24px; + width: 90%; + max-width: 400px; +} +.popup-wifi-inner button { + padding: 12px; + font-size: 1rem; + background: rgba(255,255,255,0.1); + color: #fff; + border: none; + border-radius: 10px; + cursor: pointer; +} +.popup-wifi-inner button:active { + background: rgba(255,255,255,0.2); +} +.wifi-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px; + margin: 4px 0; + background: rgba(255,255,255,0.06); + border-radius: 10px; + cursor: pointer; +} +.wifi-item:active { + background: rgba(255,255,255,0.15); +} +.wifi-item.actif { + border-left: 3px solid #4caf50; +} + /* Popup mot de passe */ .popup-overlay { position: fixed; diff --git a/frontend/index.html b/frontend/index.html index aeed0e8..570ee41 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,7 +7,7 @@ - + @@ -59,6 +59,38 @@
⚙
🖼
+ +
☰
+ + + + + @@ -1145,7 +1177,7 @@ - + diff --git a/frontend/js/app.js b/frontend/js/app.js index 1bd36a2..2efa6c6 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -707,6 +707,91 @@ function eteindreSpots() { fetch('/api/relais/veille', { method: 'POST' }).catch(() => {}); } +// --- Mini menu client --- + +function toggleMenuClient() { + document.getElementById('menu-client').classList.toggle('cache'); +} + +async function menuClientAction(action) { + document.getElementById('menu-client').classList.add('cache'); + if (action === 'wifi') { + document.getElementById('popup-wifi').classList.remove('cache'); + scanWifi(); + } else if (action === 'exposition') { + document.getElementById('popup-exposition').classList.remove('cache'); + chargerExposition(); + } else if (action === 'shutdown') { + if (confirm('Eteindre la borne ?')) apiPost('/api/systeme/eteindre'); + } else if (action === 'reboot') { + if (confirm('Redemarrer la borne ?')) apiPost('/api/systeme/redemarrer'); + } else if (action === 'restart-app') { + apiPost('/api/systeme/redemarrer-app'); + setTimeout(() => location.reload(), 3000); + } +} + +async function scanWifi() { + const list = document.getElementById('wifi-list'); + const status = document.getElementById('wifi-status'); + list.innerHTML = '
Scan en cours...
'; + try { + const s = await apiGet('/api/wifi/status'); + status.innerHTML = s.connecte + ? `
Connecte a ${s.ssid} (${s.signal || '?'}%)
` + : '
Non connecte
'; + const nets = await apiGet('/api/wifi/scan'); + if (!nets.length) { list.innerHTML = '
Aucun reseau
'; return; } + list.innerHTML = nets.map(n => ` +
+
+
${n.ssid}
+
${n.signal}% ${n.securise ? '🔒' : ''} ${n.enregistre ? '(enregistre)' : ''}
+
+ ${n.actif ? '✓' : ''} +
+ `).join(''); + } catch(e) { list.innerHTML = '
Erreur: ' + e + '
'; } +} + +async function connecterWifi(ssid, enregistre) { + if (enregistre) { + const r = await apiPost('/api/wifi/connect', {ssid}); + alert(r.message || (r.succes ? 'Connecte' : 'Erreur')); + scanWifi(); + return; + } + const mdp = prompt('Mot de passe WiFi pour ' + ssid + ' :'); + if (mdp === null) return; + const r = await apiPost('/api/wifi/connect', {ssid, password: mdp}); + alert(r.message || (r.succes ? 'Connecte' : 'Erreur')); + scanWifi(); +} + +async function chargerExposition() { + const cfg = charger_config ? charger_config() : config; + const luminosite = (cfg || config).impression?.luminosite_impression || 0; + document.getElementById('expo-slider').value = luminosite; + document.getElementById('expo-slider').min = -50; + document.getElementById('expo-slider').max = 50; + document.getElementById('expo-slider').step = 5; + updateExpoLabel(luminosite); +} + +function updateExpoLabel(val) { + const signe = val > 0 ? '+' : ''; + document.getElementById('expo-label').textContent = `${signe}${val}%`; + document.getElementById('expo-status').textContent = val == 0 ? 'Normal' : `${signe}${val}%`; +} + +async function appliquerExpo() { + const val = parseInt(document.getElementById('expo-slider').value); + await apiPost('/api/config', {impression: {luminosite_impression: val}}); + document.getElementById('popup-exposition').classList.add('cache'); + afficherStatut('Luminosite impression : ' + (val > 0 ? '+' : '') + val + '%', 'succes'); + config = await apiGet('/api/config'); +} + // Demarrage document.addEventListener('DOMContentLoaded', () => { init().then(() => {