Frontend: festes Dark-Theme, Gradient-Buttons mit Icons, Statuspille, Header-Link repariert
This commit is contained in:
1 parent
aded536568
commit
9768aae071
1 file changed
+55
-28
+55
-28
@@ -5,51 +5,69 @@
|
|||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Webcams</title>
|
<title>Webcams</title>
|
||||||
<style>
|
<style>
|
||||||
:root{--bg:#f4f5f7;--card:#fff;--fg:#1b1f24;--muted:#6a737d;--line:#dfe2e6;--accent:#2563eb;--ok:#16a34a;--bad:#dc2626}
|
:root{color-scheme:dark;--bg:#0b0e14;--bg2:#10141c;--card:#151a24;--fg:#e6e9f0;--muted:#8791a5;--line:#232a38;--accent:#4f9dff;--accent2:#8b5cf6;--ok:#34d399;--bad:#f87171;--r:14px}
|
||||||
@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}
|
*{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}
|
html,body{margin:0;min-height:100%;color:var(--fg);font:15px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
||||||
|
body{background:radial-gradient(1200px 600px at 10% -10%,#16213a 0%,transparent 60%),radial-gradient(900px 500px at 100% 0%,#1d1535 0%,transparent 55%),var(--bg);background-attachment:fixed}
|
||||||
a{color:var(--accent);text-decoration:none}
|
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{display:flex;align-items:center;gap:12px;padding:14px 22px;background:rgba(16,20,28,.72);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:5}
|
||||||
header h1{font-size:17px;margin:0;font-weight:600}
|
header h1{font-size:18px;margin:0;font-weight:700;letter-spacing:.2px;display:flex;align-items:center;gap:10px}
|
||||||
|
header h1 a{color:var(--fg)}
|
||||||
|
header h1 svg{width:24px;height:24px;color:var(--accent)}
|
||||||
header .sp{flex:1}
|
header .sp{flex:1}
|
||||||
#clock{color:var(--muted);font-variant-numeric:tabular-nums}
|
.pill{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:999px;background:var(--card);border:1px solid var(--line);color:var(--muted);font-size:13px;font-variant-numeric:tabular-nums}
|
||||||
#banner{display:none;background:var(--bad);color:#fff;padding:8px 16px;text-align:center}
|
.pill b{color:var(--ok);font-weight:600}
|
||||||
main{padding:16px;max-width:1800px;margin:0 auto}
|
#banner{display:none;background:linear-gradient(90deg,#b91c1c,#ef4444);color:#fff;padding:9px 16px;text-align:center;font-weight:500}
|
||||||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,420px),1fr));gap:16px}
|
main{padding:22px;max-width:1800px;margin:0 auto}
|
||||||
.tile{margin:0;background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden;position:relative}
|
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,420px),1fr));gap:20px}
|
||||||
|
.tile{margin:0;background:var(--card);border:1px solid var(--line);border-radius:var(--r);overflow:hidden;position:relative;box-shadow:0 8px 24px rgba(0,0,0,.35);transition:transform .15s,border-color .15s,box-shadow .15s}
|
||||||
|
.tile:hover{border-color:#34405a;box-shadow:0 12px 32px rgba(0,0,0,.5)}
|
||||||
|
.strip .tile:hover{transform:translateY(-2px)}
|
||||||
.view{position:relative;background:#000;aspect-ratio:16/9}
|
.view{position:relative;background:#000;aspect-ratio:16/9}
|
||||||
.view img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:block}
|
.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}
|
.overlay{position:absolute;inset:0;display:none;flex-direction:column;gap:8px;align-items:center;justify-content:center;background:rgba(5,7,11,.8);color:var(--muted);font-weight:500}
|
||||||
|
.overlay svg{width:34px;height:34px;color:var(--bad)}
|
||||||
.tile.off .overlay{display:flex}
|
.tile.off .overlay{display:flex}
|
||||||
.cap{display:flex;align-items:center;gap:8px;padding:8px 12px}
|
.cap{display:flex;align-items:center;gap:10px;padding:10px 14px;background:linear-gradient(180deg,var(--card),var(--bg2))}
|
||||||
.cap .name{font-weight:600}
|
.cap .name{font-weight:600;color:var(--fg)}
|
||||||
.dot{width:9px;height:9px;border-radius:50%;background:var(--muted)}
|
.cap .name:hover{color:var(--accent)}
|
||||||
.tile.on .dot{background:var(--ok)}
|
.dot{width:10px;height:10px;border-radius:50%;background:var(--muted);flex:none}
|
||||||
|
.tile.on .dot{background:var(--ok);box-shadow:0 0 0 0 rgba(52,211,153,.6);animation:pulse 2s infinite}
|
||||||
.tile.off .dot{background:var(--bad)}
|
.tile.off .dot{background:var(--bad)}
|
||||||
.state{color:var(--muted);font-size:13px}
|
@keyframes pulse{70%{box-shadow:0 0 0 7px rgba(52,211,153,0)}100%{box-shadow:0 0 0 0 rgba(52,211,153,0)}}
|
||||||
|
.state{font-size:12px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted)}
|
||||||
|
.tile.on .state{color:var(--ok)}
|
||||||
|
.tile.off .state{color:var(--bad)}
|
||||||
.cap .sp{flex:1}
|
.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}
|
.actions{display:flex;gap:8px}
|
||||||
.btn:hover{border-color:var(--accent);color:var(--accent)}
|
.btn{display:inline-flex;align-items:center;gap:6px;border:0;border-radius:9px;padding:7px 13px;font:inherit;font-size:13px;font-weight:600;color:#fff;cursor:pointer;background:linear-gradient(135deg,var(--accent),var(--accent2));box-shadow:0 2px 10px rgba(79,157,255,.25);transition:transform .1s,filter .15s,box-shadow .15s}
|
||||||
|
.btn svg{width:15px;height:15px}
|
||||||
|
.btn:hover{filter:brightness(1.15);box-shadow:0 4px 16px rgba(139,92,246,.4)}
|
||||||
|
.btn:active{transform:scale(.95)}
|
||||||
|
.btn.alt{background:#222a3a;box-shadow:none;color:var(--fg);border:1px solid var(--line)}
|
||||||
|
.btn.alt:hover{border-color:var(--accent);color:var(--accent)}
|
||||||
.focus .tile.big .view{aspect-ratio:auto;height:min(75vh,56.25vw)}
|
.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{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px;margin-top:20px}
|
||||||
.strip .cap{padding:6px 10px;font-size:13px}
|
.strip .cap{padding:8px 12px;font-size:13px}
|
||||||
|
.strip .btn{padding:5px 9px}
|
||||||
.tile:fullscreen{border:0;border-radius:0;display:flex;flex-direction:column;background:#000}
|
.tile:fullscreen{border:0;border-radius:0;display:flex;flex-direction:column;background:#000}
|
||||||
.tile:fullscreen .view{flex:1;aspect-ratio:auto}
|
.tile:fullscreen .view{flex:1;aspect-ratio:auto}
|
||||||
.tile:fullscreen .cap{background:var(--card)}
|
@media (max-width:600px){header{padding:12px 14px}main{padding:12px}.btn span{display:none}.btn{padding:7px 10px}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1><a href="__ROOT__" style="color:inherit">Webcams</a></h1>
|
<h1><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 7l-7 5 7 5V7z"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg><a id="home">Webcams</a></h1>
|
||||||
<span class="sp"></span>
|
<span class="sp"></span>
|
||||||
<span id="clock"></span>
|
<span class="pill" id="count"></span>
|
||||||
|
<span class="pill" id="clock"></span>
|
||||||
</header>
|
</header>
|
||||||
<div id="banner">Relay nicht erreichbar – neuer Versuch …</div>
|
<div id="banner">Relay nicht erreichbar – neuer Versuch …</div>
|
||||||
<main id="app"></main>
|
<main id="app"></main>
|
||||||
<script>
|
<script>
|
||||||
const ROOT = __ROOT__, CURRENT = __CURRENT__;
|
const ROOT = __ROOT__, CURRENT = __CURRENT__;
|
||||||
const app = document.getElementById("app");
|
const app = document.getElementById("app");
|
||||||
|
document.getElementById("home").href = ROOT;
|
||||||
const banner = document.getElementById("banner");
|
const banner = document.getElementById("banner");
|
||||||
const tiles = {};
|
const tiles = {};
|
||||||
let order = [], visible = true;
|
let order = [], visible = true;
|
||||||
@@ -60,7 +78,13 @@ const el = (tag, cls, text) => {
|
|||||||
if (text) e.textContent = text;
|
if (text) e.textContent = text;
|
||||||
return e;
|
return e;
|
||||||
};
|
};
|
||||||
const btn = (label, fn) => { const b = el("button", "btn", label); b.onclick = fn; return b; };
|
const ICONS = {
|
||||||
|
cam: '<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/>',
|
||||||
|
full: '<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>',
|
||||||
|
off: '<path d="M1 1l22 22M21 21H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3m3-3h6l2 3h4a2 2 0 0 1 2 2v9.34m-7.72-2.06a4 4 0 1 1-5.56-5.56"/>'
|
||||||
|
};
|
||||||
|
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; };
|
||||||
|
|
||||||
// mode: "live" = MJPEG-Stream, "thumb" = Einzelbild im Intervall
|
// mode: "live" = MJPEG-Stream, "thumb" = Einzelbild im Intervall
|
||||||
function makeTile(name, mode) {
|
function makeTile(name, mode) {
|
||||||
@@ -68,7 +92,8 @@ function makeTile(name, mode) {
|
|||||||
const view = el("div", "view");
|
const view = el("div", "view");
|
||||||
const img = el("img");
|
const img = el("img");
|
||||||
img.alt = name;
|
img.alt = name;
|
||||||
const ov = el("div", "overlay", "Kamera offline");
|
const ov = el("div", "overlay");
|
||||||
|
ov.append(icon("off"), el("span", "", "Kamera offline"));
|
||||||
view.append(img, ov);
|
view.append(img, ov);
|
||||||
|
|
||||||
const cap = el("figcaption", "cap");
|
const cap = el("figcaption", "cap");
|
||||||
@@ -76,14 +101,14 @@ function makeTile(name, mode) {
|
|||||||
link.href = ROOT + name + "/";
|
link.href = ROOT + name + "/";
|
||||||
const state = el("span", "state", "verbinde …");
|
const state = el("span", "state", "verbinde …");
|
||||||
const actions = el("span", "actions");
|
const actions = el("span", "actions");
|
||||||
const snap = btn("Foto", () => {
|
const snap = btn("Foto", "cam", () => {
|
||||||
const a = el("a");
|
const a = el("a");
|
||||||
a.href = ROOT + name + "/snapshot.jpg?t=" + Date.now();
|
a.href = ROOT + name + "/snapshot.jpg?t=" + Date.now();
|
||||||
a.download = name + "-" + new Date().toISOString().replace(/[:.]/g, "-") + ".jpg";
|
a.download = name + "-" + new Date().toISOString().replace(/[:.]/g, "-") + ".jpg";
|
||||||
a.click();
|
a.click();
|
||||||
});
|
});
|
||||||
const full = btn("Vollbild", () => document.fullscreenElement ? document.exitFullscreen() : t.requestFullscreen());
|
const full = btn("Vollbild", "full", () => document.fullscreenElement ? document.exitFullscreen() : t.requestFullscreen(), "alt");
|
||||||
actions.append(snap, " ", full);
|
actions.append(snap, full);
|
||||||
cap.append(el("span", "dot"), link, state, el("span", "sp"), actions);
|
cap.append(el("span", "dot"), link, state, el("span", "sp"), actions);
|
||||||
t.append(view, cap);
|
t.append(view, cap);
|
||||||
|
|
||||||
@@ -146,6 +171,8 @@ async function poll() {
|
|||||||
const names = data.cameras.map(c => c.name);
|
const names = data.cameras.map(c => c.name);
|
||||||
if (names.join() !== order.join()) build(names);
|
if (names.join() !== order.join()) build(names);
|
||||||
data.cameras.forEach(c => setOnline(tiles[c.name], c.online));
|
data.cameras.forEach(c => setOnline(tiles[c.name], c.online));
|
||||||
|
const n = data.cameras.filter(c => c.online).length;
|
||||||
|
document.getElementById("count").innerHTML = "<b>" + n + "</b> / " + data.cameras.length + " online";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
banner.style.display = "block";
|
banner.style.display = "block";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user