diff --git a/.env.example b/.env.example index 199abcd..e2da8e5 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,7 @@ CAMERA_cam2=http://IP:PORT/pfad CAMERA_cam3=http://IP:PORT/pfad # Optional: Konfigurations-URL pro Kamera (Button "Setup" im Frontend), z.B. ESP32-CAM Auflösung -#SETUP_cam1=http://IP/control?var=framesize&val=11 +#SETUP_cam2=http://IP/control?var=framesize&val=11 PORT=8000 # Optional: Login (nur sinnvoll mit HTTPS in Zoraxy) diff --git a/frontend.html b/frontend.html index cb10bd0..f22c9c9 100644 --- a/frontend.html +++ b/frontend.html @@ -61,6 +61,7 @@ main{padding:22px;max-width:1800px;margin:0 auto}

Webcams

+
@@ -72,6 +73,7 @@ const app = document.getElementById("app"); document.getElementById("home").href = ROOT; const banner = document.getElementById("banner"); const tiles = {}; +let setupNames = []; let order = [], visible = true; const el = (tag, cls, text) => { @@ -88,6 +90,15 @@ const ICONS = { }; const icon = k => { const s = document.createElementNS("http://www.w3.org/2000/svg", "svg"); s.setAttribute("viewBox", "0 0 24 24"); s.setAttribute("fill", "none"); s.setAttribute("stroke", "currentColor"); s.setAttribute("stroke-width", "2"); s.setAttribute("stroke-linecap", "round"); s.setAttribute("stroke-linejoin", "round"); s.innerHTML = ICONS[k]; return s; }; const btn = (label, ic, fn, cls) => { const b = el("button", "btn" + (cls ? " " + cls : "")); b.title = label; b.append(icon(ic), el("span", "", label)); b.onclick = fn; return b; }; +const setupAllBtn = document.getElementById("setupAll"); +setupAllBtn.prepend(icon("cog")); +setupAllBtn.onclick = async () => { + setupAllBtn.disabled = true; + const label = setupAllBtn.querySelector("span"); + const res = await Promise.all(setupNames.map(runSetup)); + label.textContent = res.every(Boolean) ? "Gesendet ✓" : "Fehler ✗"; + setTimeout(() => { label.textContent = "Setup alle"; setupAllBtn.disabled = false; }, 2500); +}; // mode: "live" = MJPEG-Stream, "thumb" = Einzelbild im Intervall function makeTile(name, mode, hasSetup) { @@ -115,13 +126,7 @@ function makeTile(name, mode, hasSetup) { const setup = btn("Setup", "cog", async () => { setup.disabled = true; const label = setup.querySelector("span"); - try { - const r = await fetch(ROOT + name + "/setup", { method: "POST" }); - if (!r.ok) throw new Error(r.status); - label.textContent = "Gesendet ✓"; - } catch (e) { - label.textContent = "Fehler ✗"; - } + label.textContent = (await runSetup(name)) ? "Gesendet ✓" : "Fehler ✗"; setTimeout(() => { label.textContent = "Setup"; setup.disabled = false; }, 2500); }, "alt"); actions.append(setup); @@ -140,6 +145,18 @@ function makeTile(name, mode, hasSetup) { return t; } +async function runSetup(name) { + try { + const r = await fetch(ROOT + name + "/setup", { method: "POST" }); + if (!r.ok) throw new Error(r.status); + // Kamera startet den Stream mit neuer Auflösung; Bild kurz danach neu laden + setTimeout(() => tiles[name] && connect(tiles[name]), 800); + return true; + } catch (e) { + return false; + } +} + function connect(o) { clearTimeout(o.retry); clearInterval(o.timer); @@ -192,6 +209,8 @@ async function poll() { if (names.join() !== order.join()) build(data.cameras); data.cameras.forEach(c => setOnline(tiles[c.name], c.online)); const n = data.cameras.filter(c => c.online).length; + setupNames = data.cameras.filter(c => c.setup).map(c => c.name); + setupAllBtn.hidden = !setupNames.length; document.getElementById("count").innerHTML = "" + n + " / " + data.cameras.length + " online"; } catch (e) { banner.style.display = "block";