From 0b2837f0c34ab60e766fb044c850013b4ce05909 Mon Sep 17 00:00:00 2001 From: Joachim Hummel Date: Sun, 18 Oct 2026 13:13:16 +0200 Subject: [PATCH] Frontend: Kacheln per Drag-and-drop umsortieren (Reihenfolge in localStorage) --- frontend.html | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/frontend.html b/frontend.html index 7329ba1..91deb06 100644 --- a/frontend.html +++ b/frontend.html @@ -57,6 +57,10 @@ main{padding:22px;max-width:1800px;margin:0 auto} .btn.ok:hover{box-shadow:0 4px 16px rgba(34,197,94,.45)} .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)} +.grid .cap{cursor:grab} +.grid .cap .btn{cursor:pointer} +.tile.dragging{opacity:.4} +.tile.dragover{border-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(220px,1fr));gap:16px;margin-top:20px} .strip .cap{padding:8px 12px;font-size:13px} @@ -232,6 +236,52 @@ function stopTile(o) { function reloadTile(o) { stopTile(o); setTimeout(() => startTile(o), 300); } const liveTiles = () => Object.values(tiles).filter(o => o.mode === "live"); +const ORDER_KEY = "webcam-order"; +function savedOrder() { + try { return JSON.parse(localStorage.getItem(ORDER_KEY)) || []; } catch (e) { return []; } +} +function sortBySaved(names) { + const saved = savedOrder(); + const rank = n => { const i = saved.indexOf(n); return i < 0 ? saved.length : i; }; + return names.map((n, i) => [n, i]).sort((a, b) => rank(a[0]) - rank(b[0]) || a[1] - b[1]).map(x => x[0]); +} +// Kacheln per Drag-and-drop an der Beschriftungsleiste umsortieren +function enableReorder(grid) { + let dragged = null; + grid.querySelectorAll(".tile").forEach(t => { + const cap = t.querySelector(".cap"); + cap.draggable = true; + cap.addEventListener("dragstart", e => { + dragged = t; + t.classList.add("dragging"); + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", t.querySelector(".name").textContent); + e.dataTransfer.setDragImage(t, 20, 20); + }); + cap.addEventListener("dragend", () => { + t.classList.remove("dragging"); + grid.querySelectorAll(".dragover").forEach(x => x.classList.remove("dragover")); + dragged = null; + }); + t.addEventListener("dragover", e => { + if (!dragged || dragged === t) return; + e.preventDefault(); + t.classList.add("dragover"); + }); + t.addEventListener("dragleave", () => t.classList.remove("dragover")); + t.addEventListener("drop", e => { + e.preventDefault(); + t.classList.remove("dragover"); + if (!dragged || dragged === t) return; + const kids = [...grid.children]; + grid.insertBefore(dragged, kids.indexOf(dragged) < kids.indexOf(t) ? t.nextSibling : t); + try { + localStorage.setItem(ORDER_KEY, JSON.stringify([...grid.children].map(c => c.querySelector(".name").textContent))); + } catch (err) {} + }); + }); +} + function build(cams) { const names = cams.map(c => c.name); const setupOf = n => cams.find(c => c.name === n).setup; @@ -247,8 +297,9 @@ function build(cams) { if (strip.children.length) app.append(strip); } else { const grid = el("div", "grid"); - names.forEach(n => grid.append(makeTile(n, "live", setupOf(n)))); + sortBySaved(names).forEach(n => grid.append(makeTile(n, "live", setupOf(n)))); app.append(grid); + enableReorder(grid); } }