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 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 10:22:00 +02:00
parent bb49c2c82d
commit 5e8cff7951
5 changed files with 297 additions and 3 deletions

View File

@@ -495,10 +495,30 @@ async def _reconnecter_dslr_avec_reset(echecs: int):
def _appliquer_config_camera(): 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() cfg = charger_config()
flash_integre = cfg.get("camera", {}).get("flash_integre", True) flash_integre = cfg.get("camera", {}).get("flash_integre", True)
camera.configurer_flash(flash_integre) 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(): async def _watchdog_systemd():
@@ -1260,6 +1280,55 @@ async def api_camera_reconnecter(body: dict = {}):
return {"connectee": ok, "mode": camera.mode} 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 --- # --- API Effets ---
@app.get("/api/filtres") @app.get("/api/filtres")

View File

@@ -111,6 +111,13 @@ def _preparer_image(chemin: Path, largeur: int, hauteur: int,
if config_imp.get("rotation_180", False) and not skip_rotate: if config_imp.get("rotation_180", False) and not skip_rotate:
img = img.rotate(180) 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) tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
img.save(tmp.name, "JPEG", quality=95, dpi=(300, 300)) img.save(tmp.name, "JPEG", quality=95, dpi=(300, 300))
log.debug(f"Image préparée : {orientation} {largeur}x{hauteur}px → {tmp.name}") log.debug(f"Image préparée : {orientation} {largeur}x{hauteur}px → {tmp.name}")

View File

@@ -157,6 +157,107 @@ html, body {
color: rgba(255,255,255,0.4); 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 mot de passe */
.popup-overlay { .popup-overlay {
position: fixed; position: fixed;

View File

@@ -7,7 +7,7 @@
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"> <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta name="google" content="notranslate"> <meta name="google" content="notranslate">
<meta http-equiv="Content-Language" content="fr"> <meta http-equiv="Content-Language" content="fr">
<link rel="stylesheet" href="/css/style.css?v=16"> <link rel="stylesheet" href="/css/style.css?v=17">
<link rel="stylesheet" href="/css/themes.css?v=2"> <link rel="stylesheet" href="/css/themes.css?v=2">
</head> </head>
<body> <body>
@@ -59,6 +59,38 @@
<div id="btn-admin" class="btn-admin" onclick="ouvrirAdmin()">&#9881;</div> <div id="btn-admin" class="btn-admin" onclick="ouvrirAdmin()">&#9881;</div>
<!-- Icone photostation coin bas-gauche (visible seulement si installée) --> <!-- Icone photostation coin bas-gauche (visible seulement si installée) -->
<div id="btn-photostation" class="btn-photostation cache" onclick="ouvrirPhotostation()">&#128444;</div> <div id="btn-photostation" class="btn-photostation cache" onclick="ouvrirPhotostation()">&#128444;</div>
<!-- Mini menu client coin haut-gauche -->
<div id="btn-menu-client" class="btn-menu-client" onclick="toggleMenuClient()">&#9776;</div>
<div id="menu-client" class="menu-client cache">
<div class="menu-client-titre">Maintenance</div>
<button onclick="menuClientAction('wifi')">&#128246; WiFi</button>
<button onclick="menuClientAction('exposition')">&#9728; Luminosite</button>
<button onclick="menuClientAction('restart-app')">&#128260; Relancer l'appli</button>
<button onclick="menuClientAction('reboot')">&#128259; Redemarrer</button>
<button onclick="menuClientAction('shutdown')">&#9211; Eteindre</button>
<button onclick="toggleMenuClient()" style="background:transparent;color:var(--text2)">Fermer</button>
</div>
<!-- Popup WiFi -->
<div id="popup-wifi" class="popup-wifi cache">
<div class="popup-wifi-inner">
<div class="menu-client-titre">WiFi</div>
<div id="wifi-status"></div>
<div id="wifi-list" style="max-height:300px;overflow-y:auto"></div>
<button onclick="scanWifi()" style="margin-top:8px;width:100%">Actualiser</button>
<button onclick="document.getElementById('popup-wifi').classList.add('cache')" style="margin-top:4px;width:100%;background:transparent;color:var(--text2)">Fermer</button>
</div>
</div>
<!-- Popup Exposition -->
<div id="popup-exposition" class="popup-wifi cache">
<div class="popup-wifi-inner">
<div class="menu-client-titre">Luminosite photo</div>
<div id="expo-status" style="text-align:center;margin:12px 0;font-size:1.5rem;font-weight:700"></div>
<input type="range" id="expo-slider" min="-9" max="9" value="0" style="width:100%" oninput="updateExpoLabel(this.value)">
<div id="expo-label" style="text-align:center;margin:8px 0;font-size:.9rem;color:var(--text2)">0</div>
<button onclick="appliquerExpo()" style="width:100%">Appliquer</button>
<button onclick="document.getElementById('popup-exposition').classList.add('cache')" style="margin-top:4px;width:100%;background:transparent;color:var(--text2)">Fermer</button>
</div>
</div>
</section> </section>
<!-- Choix du mode --> <!-- Choix du mode -->
@@ -1145,7 +1177,7 @@
</style> </style>
<script src="/js/websocket.js?v=16"></script> <script src="/js/websocket.js?v=16"></script>
<script src="/js/app.js?v=25"></script> <script src="/js/app.js?v=26"></script>
<script src="/js/camera.js?v=26"></script> <script src="/js/camera.js?v=26"></script>
<script src="/js/effects.js?v=4"></script> <script src="/js/effects.js?v=4"></script>
<script src="/js/gallery.js?v=5"></script> <script src="/js/gallery.js?v=5"></script>

View File

@@ -707,6 +707,91 @@ function eteindreSpots() {
fetch('/api/relais/veille', { method: 'POST' }).catch(() => {}); 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 = '<div style="text-align:center;padding:16px;color:var(--text2)">Scan en cours...</div>';
try {
const s = await apiGet('/api/wifi/status');
status.innerHTML = s.connecte
? `<div style="padding:8px;color:#4caf50">Connecte a <b>${s.ssid}</b> (${s.signal || '?'}%)</div>`
: '<div style="padding:8px;color:#f44336">Non connecte</div>';
const nets = await apiGet('/api/wifi/scan');
if (!nets.length) { list.innerHTML = '<div style="padding:12px;color:var(--text2)">Aucun reseau</div>'; return; }
list.innerHTML = nets.map(n => `
<div class="wifi-item${n.actif ? ' actif' : ''}" onclick="connecterWifi('${n.ssid.replace(/'/g,"\\'")}', ${n.enregistre})">
<div>
<div style="font-weight:600">${n.ssid}</div>
<div style="font-size:.75rem;color:var(--text2)">${n.signal}% ${n.securise ? '&#128274;' : ''} ${n.enregistre ? '(enregistre)' : ''}</div>
</div>
${n.actif ? '<span style="color:#4caf50;font-weight:700">&#10003;</span>' : ''}
</div>
`).join('');
} catch(e) { list.innerHTML = '<div style="padding:12px;color:#f44336">Erreur: ' + e + '</div>'; }
}
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 // Demarrage
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
init().then(() => { init().then(() => {