/* ── Der Widget-Rahmen ───────────────────────────────────────────────────── * * Ein Widget ist hier eine **Beschreibung**, kein Stück HTML im Dokument. * Vorher standen Benutzer und Wetter fest in index.html, jedes mit eigener * Zeigen-und-Verstecken-Logik in seiner Datei — beim dritten Widget hätte man * dieselben zwanzig Zeilen ein drittes Mal geschrieben. * * Der Rahmen kümmert sich um das, was jedes Widget braucht und keines für sich * lösen sollte: * * · beide Betriebsarten — Fenster im Terminal, Schild auf dem Schreibtisch * · die vier Zustände: lädt · fertig · nichts da · Fehler * · Reihenfolge und Nachladen im Takt * * Ein Widget beschreibt sich so: * * { * id, titel, * wo: ["terminal", "desktop"] // wo es erscheinen darf * sichtbar: () => true // Schalter aus → gar nicht erst da * laden: async () => false|void // false = es gibt nichts zu zeigen * leer: "verbergen" | "Text" // was dann passiert * inhalt: () => "" * takt: 600000 // ms, optional * } * * Absichtlich offen gelassen: eine dritte Form. Die Widgets sollen später auch * als Aufklappfeld in der HoneyBar leben können — deshalb erzeugt `inhalt()` * nur den Kern, und die Verpackung entsteht hier. */ import { $, state } from "../core.js"; const REGISTER = []; /** id → "laedt" | "fertig" | "leer" | "fehler" */ const zustand = {}; const uhren = {}; export function widgetAnmelden(def) { REGISTER.push(def); zustand[def.id] = def.laden ? "laedt" : "fertig"; } /** Nur zum Nachsehen von außen — die Liste selbst gehört dem Rahmen. */ export function widgetListe() { return REGISTER.map((w) => w.id); } /** * Legt die Reihenfolge fest. Was nicht genannt ist, hängt sich hinten an. * * Warum überhaupt? Weil die Reihenfolge sonst davon abhinge, in welcher * Reihenfolge die Module ausgewertet werden — und das ist **nicht** die * Reihenfolge der Importzeilen: `glyphs.js` lädt seine Raster mit `await` auf * oberster Ebene, also warten alle Module, die es brauchen, während die * anderen schon durchlaufen. Der Systemzustand braucht keine Symbole und stand * deshalb vor dem Benutzer, obwohl er darunter importiert wird. * * Hier ist die Reihenfolge damit eine Angabe statt einer Nebenwirkung — und * genau die Stelle, an der später die im Server gespeicherte Reihenfolge * einhaken kann. */ export function widgetsOrdnen(ids) { const platz = (w) => { const i = ids.indexOf(w.id); return i < 0 ? ids.length : i; }; REGISTER.sort((a, b) => platz(a) - platz(b)); } /** * Lädt ein Widget (oder alle) und zeichnet neu. * * Fehler bleiben hier: Ein Widget, das seinen Server nicht erreicht, sagt das * in seinem eigenen Rahmen — es darf weder den Schreibtisch aufhalten noch die * anderen Widgets mitreißen. */ export async function widgetsLaden(nurId) { const dran = REGISTER.filter((w) => w.laden && (!nurId || w.id === nurId)); await Promise.all( dran.map(async (w) => { zustand[w.id] = "laedt"; widgetsZeichnen(); try { zustand[w.id] = (await w.laden()) === false ? "leer" : "fertig"; } catch { zustand[w.id] = "fehler"; } }) ); widgetsZeichnen(); } /** Startet die Nachlade-Takte. Einmal beim Aufbau des Schreibtischs. */ export function widgetsStarten() { REGISTER.forEach((w) => { if (!w.takt || uhren[w.id]) return; uhren[w.id] = setInterval(() => widgetsLaden(w.id), w.takt); }); } function zustandHtml(w) { switch (zustand[w.id]) { case "laedt": return `
lädt …
`; case "fehler": return `nicht erreichbar
`; case "leer": return `${w.leer}
`; default: return w.inhalt(); } } /** * Zeichnet alle Widgets in die Ansicht, die gerade gilt. * * Jedes Widget bekommt seinen eigenen Knoten, der stehen bleibt, solange es * gebraucht wird. Der erste Entwurf hat beide Behälter bei jedem Durchgang neu * gefüllt — kürzer, aber falsch, sobald ein Widget etwas im DOM hält: Der * Zettel wird alle zwei Minuten mitgezeichnet (die Apps werden dann angeklopft), * und wer gerade tippte, verlor Text und Schreibmarke. * * Deshalb zwei Zusagen an die Widgets: * · `stabil: true` — dein Inhalt wird nicht neu gesetzt, solange sich dein * Zustand nicht ändert. Du führst ihn selbst nach. * · `montiert(el)` — wird nach jedem Neusetzen gerufen, zum Verdrahten. */ function baue(behaelter, dran, alsFenster) { const alt = new Map([...behaelter.children].map((el) => [el.dataset.widget, el])); dran.forEach((w) => { let el = alt.get(w.id); alt.delete(w.id); if (!el) { el = document.createElement(alsFenster ? "section" : "div"); el.dataset.widget = w.id; el.className = alsFenster ? "win win-widget" : "deskwidget"; if (alsFenster) { el.innerHTML = ``; } behaelter.appendChild(el); } const koerper = alsFenster ? el.querySelector(".win-body") : el; const frisch = el.dataset.zustand !== zustand[w.id]; if (frisch || !w.stabil) { koerper.innerHTML = zustandHtml(w); el.dataset.zustand = zustand[w.id]; if (w.montiert && zustand[w.id] === "fertig") w.montiert(koerper); } }); alt.forEach((el) => el.remove()); // Reihenfolge nur anfassen, wenn sie nicht stimmt: Einen Knoten zu verschieben // heißt, ihn kurz aus dem Dokument zu nehmen — ein Textfeld verliert dabei // die Schreibmarke. const soll = dran.map((w) => w.id).join(","); const ist = [...behaelter.children].map((el) => el.dataset.widget).join(","); if (soll !== ist) { dran.forEach((w) => behaelter.appendChild(behaelter.querySelector(`[data-widget="${w.id}"]`))); } } /** * Wählt aus, was gerade dran ist, und lässt es zeichnen. */ export function widgetsZeichnen() { const desktop = state.settings.mode === "desktop"; const modus = desktop ? "desktop" : "terminal"; const spalte = $("widgetSpalte"); const schilder = $("deskwidgets"); if (!spalte || !schilder) return; const dran = REGISTER.filter((w) => { if (w.wo && !w.wo.includes(modus)) return false; if (w.sichtbar && !w.sichtbar()) return false; // „nichts da" heißt bei manchen Widgets: gar nicht erst hinstellen. Eine // leere Wetterkachel wäre schlechter als keine. return !(zustand[w.id] === "leer" && w.leer === "verbergen"); }); baue(spalte, desktop ? [] : dran, true); baue(schilder, desktop ? dran : [], false); }