aboutsummaryrefslogtreecommitdiffstats
path: root/static/app.js
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-09 19:14:30 +0100
committerDanilo M. <danix@danix.xyz>2026-11-09 19:14:30 +0100
commitffd2799fafa1a34c3b75b9cb8f7f18ef58b309d4 (patch)
tree21ab542297160755f9280236e26875fb3680d780 /static/app.js
parent15e384bc1d97c84df0545555bf4c298702c9eacb (diff)
downloaddeck-it-ffd2799fafa1a34c3b75b9cb8f7f18ef58b309d4.tar.gz
deck-it-ffd2799fafa1a34c3b75b9cb8f7f18ef58b309d4.zip
feat: tabbed layout with OBS as an icon-forward stream-deck
Diffstat (limited to 'static/app.js')
-rw-r--r--static/app.js132
1 files changed, 97 insertions, 35 deletions
diff --git a/static/app.js b/static/app.js
index c7730e3..54d4b26 100644
--- a/static/app.js
+++ b/static/app.js
@@ -1,22 +1,18 @@
-// The shell: discover modules, mount each panel as a Gridstack card, poll its
-// state. A module is static/modules/<name>.js exporting default {title,
-// interval, mount(el), update(state, el)}. Card position and size are kept in
-// this browser's localStorage.
+// The shell: discover modules, sort them into tabs, mount each panel and poll
+// its state. A module is static/modules/<name>.js exporting default {title,
+// tab, interval, mount(el), update(state, el)}. Modules without a tab go into
+// the Dashboard. Card order and size are kept in this browser's localStorage.
-const gridEl = document.getElementById("grid");
+const tabBar = document.getElementById("tabs");
+const tabBody = document.getElementById("tab-body");
const refreshers = [];
const LAYOUT_KEY = "deckit.layout";
+const TAB_KEY = "deckit.tab";
const DEFAULT_W = 6;
const DEFAULT_H = 4;
+const tabs = {};
-const grid = GridStack.init({
- column: 12,
- cellHeight: 80,
- margin: 8,
- float: true,
- handle: ".panel-drag",
- resizable: { handles: "se" },
-}, gridEl);
+let ready = false;
function loadLayout() {
try {
@@ -36,7 +32,51 @@ function saveLayout() {
}
}
-let ready = false;
+function ensureTab(name) {
+ if (tabs[name]) return tabs[name];
+ const button = document.createElement("button");
+ button.textContent = name;
+ button.onclick = () => showTab(name);
+ tabBar.append(button);
+ const section = document.createElement("section");
+ section.className = "tab-panel";
+ section.dataset.tab = name;
+ section.hidden = true;
+ tabBody.append(section);
+ tabs[name] = { button, section };
+ return tabs[name];
+}
+
+function showTab(name) {
+ for (const [n, tab] of Object.entries(tabs)) {
+ tab.section.hidden = n !== name;
+ tab.button.classList.toggle("active", n === name);
+ }
+ try {
+ localStorage.setItem(TAB_KEY, name);
+ } catch {}
+ if (name === "Dashboard") window.dispatchEvent(new Event("resize"));
+}
+
+// Dashboard is the grid; it exists before the modules so Gridstack measures a
+// visible container.
+const dashboard = ensureTab("Dashboard");
+dashboard.section.hidden = false;
+dashboard.button.classList.add("active");
+
+const gridEl = document.createElement("div");
+gridEl.id = "grid";
+gridEl.className = "grid-stack";
+dashboard.section.append(gridEl);
+
+const grid = GridStack.init({
+ column: 12,
+ cellHeight: 80,
+ margin: 8,
+ float: true,
+ handle: ".panel-drag",
+ resizable: { handles: "se" },
+}, gridEl);
grid.on("change", saveLayout);
const savedById = {};
@@ -81,33 +121,55 @@ function makeCard(name, title) {
return body;
}
-async function mountModule(name) {
- try {
- const panel = (await import(`./modules/${name}.js`)).default;
- const content = makeCard(name, panel.title ?? name);
- panel.mount?.(content);
-
- const poll = async () => {
- try {
- const r = await fetch(`/api/${name}`);
- if (r.ok) panel.update?.(await r.json(), content);
- } catch {
- // keep the last state; the next tick retries
- }
- };
- refreshers.push(poll);
- await poll();
- setInterval(poll, (panel.interval ?? 1) * 1000);
- } catch {
- // a broken panel must not take down the rest of the dashboard
- }
+async function startPoll(name, panel, el) {
+ const poll = async () => {
+ try {
+ const r = await fetch(`/api/${name}`);
+ if (r.ok) panel.update?.(await r.json(), el);
+ } catch {
+ // keep the last state; the next tick retries
+ }
+ };
+ refreshers.push(poll);
+ await poll();
+ setInterval(poll, (panel.interval ?? 1) * 1000);
+}
+
+async function mountDashboard(name, panel) {
+ const body = makeCard(name, panel.title ?? name);
+ panel.mount?.(body);
+ await startPoll(name, panel, body);
+}
+
+async function mountTab(name, panel, section) {
+ const wrap = document.createElement("div");
+ wrap.className = "tab-content";
+ section.append(wrap);
+ panel.mount?.(wrap);
+ await startPoll(name, panel, wrap);
}
async function boot() {
const r = await fetch("/api/modules");
if (!r.ok) return;
- for (const { name } of await r.json()) await mountModule(name);
+ for (const { name } of await r.json()) {
+ let panel;
+ try {
+ panel = (await import(`./modules/${name}.js`)).default;
+ } catch {
+ continue;
+ }
+ const tabName = panel.tab ?? "Dashboard";
+ try {
+ if (tabName === "Dashboard") await mountDashboard(name, panel);
+ else await mountTab(name, panel, ensureTab(tabName).section);
+ } catch {
+ // a broken panel must not take down the rest of the dashboard
+ }
+ }
ready = true;
+ const saved = localStorage.getItem(TAB_KEY);
+ showTab(tabs[saved] ? saved : "Dashboard");
}
document.addEventListener("visibilitychange", () => {