Files
webcam-server/frontend.html
T

167 lines
6.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Webcams</title>
<style>
:root{--bg:#f4f5f7;--card:#fff;--fg:#1b1f24;--muted:#6a737d;--line:#dfe2e6;--accent:#2563eb;--ok:#16a34a;--bad:#dc2626}
@media (prefers-color-scheme:dark){:root{--bg:#0f1114;--card:#181b20;--fg:#e8eaed;--muted:#8b949e;--line:#2a2f36;--accent:#6ea8ff;--ok:#3fb950;--bad:#f85149}}
*{box-sizing:border-box}
html,body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.4 system-ui,-apple-system,"Segoe UI",sans-serif}
a{color:var(--accent);text-decoration:none}
header{display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--line);background:var(--card);position:sticky;top:0;z-index:5}
header h1{font-size:17px;margin:0;font-weight:600}
header .sp{flex:1}
#clock{color:var(--muted);font-variant-numeric:tabular-nums}
#banner{display:none;background:var(--bad);color:#fff;padding:8px 16px;text-align:center}
main{padding:16px;max-width:1800px;margin:0 auto}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,420px),1fr));gap:16px}
.tile{margin:0;background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden;position:relative}
.view{position:relative;background:#000;aspect-ratio:16/9}
.view img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:block}
.overlay{position:absolute;inset:0;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,.65);color:#fff;font-weight:500}
.tile.off .overlay{display:flex}
.cap{display:flex;align-items:center;gap:8px;padding:8px 12px}
.cap .name{font-weight:600}
.dot{width:9px;height:9px;border-radius:50%;background:var(--muted)}
.tile.on .dot{background:var(--ok)}
.tile.off .dot{background:var(--bad)}
.state{color:var(--muted);font-size:13px}
.cap .sp{flex:1}
.btn{border:1px solid var(--line);background:transparent;color:var(--fg);border-radius:6px;padding:4px 9px;font:inherit;font-size:13px;cursor:pointer}
.btn:hover{border-color:var(--accent);color:var(--accent)}
.focus .tile.big .view{aspect-ratio:auto;height:min(75vh,56.25vw)}
.strip{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;margin-top:16px}
.strip .cap{padding:6px 10px;font-size:13px}
.tile:fullscreen{border:0;border-radius:0;display:flex;flex-direction:column;background:#000}
.tile:fullscreen .view{flex:1;aspect-ratio:auto}
.tile:fullscreen .cap{background:var(--card)}
</style>
</head>
<body>
<header>
<h1><a href="__ROOT__" style="color:inherit">Webcams</a></h1>
<span class="sp"></span>
<span id="clock"></span>
</header>
<div id="banner">Relay nicht erreichbar – neuer Versuch …</div>
<main id="app"></main>
<script>
const ROOT = __ROOT__, CURRENT = __CURRENT__;
const app = document.getElementById("app");
const banner = document.getElementById("banner");
const tiles = {};
let order = [], visible = true;
const el = (tag, cls, text) => {
const e = document.createElement(tag);
if (cls) e.className = cls;
if (text) e.textContent = text;
return e;
};
const btn = (label, fn) => { const b = el("button", "btn", label); b.onclick = fn; return b; };
// mode: "live" = MJPEG-Stream, "thumb" = Einzelbild im Intervall
function makeTile(name, mode) {
const t = el("figure", "tile");
const view = el("div", "view");
const img = el("img");
img.alt = name;
const ov = el("div", "overlay", "Kamera offline");
view.append(img, ov);
const cap = el("figcaption", "cap");
const link = el("a", "name", name);
link.href = ROOT + name + "/";
const state = el("span", "state", "verbinde …");
const actions = el("span", "actions");
const snap = btn("Foto", () => {
const a = el("a");
a.href = ROOT + name + "/snapshot.jpg?t=" + Date.now();
a.download = name + "-" + new Date().toISOString().replace(/[:.]/g, "-") + ".jpg";
a.click();
});
const full = btn("Vollbild", () => document.fullscreenElement ? document.exitFullscreen() : t.requestFullscreen());
actions.append(snap, " ", full);
cap.append(el("span", "dot"), link, state, el("span", "sp"), actions);
t.append(view, cap);
const o = { name, mode, t, img, state, online: null, timer: null, retry: null };
img.onerror = () => {
if (!visible || o.online === false) return;
clearTimeout(o.retry);
o.retry = setTimeout(() => connect(o), 3000);
};
tiles[name] = o;
return t;
}
function connect(o) {
clearTimeout(o.retry);
clearInterval(o.timer);
if (!visible) { o.img.removeAttribute("src"); return; }
const base = ROOT + o.name + "/";
if (o.mode === "live") {
o.img.src = base + "stream.mjpg?t=" + Date.now();
} else {
const load = () => { o.img.src = base + "snapshot.jpg?t=" + Date.now(); };
load();
o.timer = setInterval(load, 3000);
}
}
function setOnline(o, on) {
o.t.classList.toggle("on", on);
o.t.classList.toggle("off", !on);
o.state.textContent = on ? "live" : "offline";
if (on && o.online !== true) connect(o);
o.online = on;
}
function build(names) {
order = names;
app.replaceChildren();
if (CURRENT && names.includes(CURRENT)) {
app.className = "focus";
const big = makeTile(CURRENT, "live");
big.classList.add("big");
app.append(big);
const strip = el("div", "strip");
names.filter(n => n !== CURRENT).forEach(n => strip.append(makeTile(n, "thumb")));
if (strip.children.length) app.append(strip);
} else {
const grid = el("div", "grid");
names.forEach(n => grid.append(makeTile(n, "live")));
app.append(grid);
}
}
async function poll() {
try {
const r = await fetch(ROOT + "status.json", { cache: "no-store" });
if (!r.ok) throw new Error(r.status);
const data = await r.json();
banner.style.display = "none";
const names = data.cameras.map(c => c.name);
if (names.join() !== order.join()) build(names);
data.cameras.forEach(c => setOnline(tiles[c.name], c.online));
} catch (e) {
banner.style.display = "block";
}
}
// Verbindungen im Hintergrund-Tab trennen, spart Bandbreite
document.addEventListener("visibilitychange", () => {
visible = !document.hidden;
Object.values(tiles).forEach(o => { if (!visible || o.online) connect(o); });
});
const clock = document.getElementById("clock");
const tick = () => { clock.textContent = new Date().toLocaleString("de-DE"); };
tick(); setInterval(tick, 1000);
poll(); setInterval(poll, 5000);
</script>
</body>
</html>