/* ── 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 { $, saveSettings, state } from "../core.js"; const REGISTER = []; /** id → "laedt" | "fertig" | "leer" | "fehler" */ const zustand = {}; const uhren = {}; /** Was schon einmal geladen wurde — ein Widget, das erst später eingeschaltet * wird, holt seine Daten dann nach. */ const geladen = new Set(); 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. Sie ist * die **Grundstellung**: Was der Nutzer selbst sortiert hat, steht in seinen * Einstellungen und schlägt sie (siehe widgetPlan). */ 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)); } /* ── Was der Nutzer eingestellt hat ──────────────────────────────────────── * * Reihenfolge und An/Aus stehen in **einer** Einstellung, als Liste von IDs mit * einem Minus vor dem, was aus ist: * * widgets = "benutzer,system,-wetter,notiz,bild" * * Ein Feld statt zwei, weil beides zusammen gehört: Wer ein Widget ausschaltet, * soll seinen Platz behalten, wenn er es wieder einschaltet. Und weil die * Einstellungen ohnehin schon je Nutzer auf dem Server liegen, braucht es dafür * weder eine neue Tabelle noch einen neuen Endpunkt. * * Die gespeicherte Liste ist ein Wunsch, kein Gesetz: Unbekannte IDs fallen * weg (ein Widget kann verschwinden), neue hängen sich hinten an und sind an. */ const AUS = "-"; /** Die Widgets in der Reihenfolge des Nutzers: [{id, titel, wo, an}]. */ export function widgetPlan() { const aus = new Set(); const reihe = []; String(state.settings.widgets || "") .split(",") .map((e) => e.trim()) .filter(Boolean) .forEach((e) => { const id = e.startsWith(AUS) ? e.slice(1) : e; if (e.startsWith(AUS)) aus.add(id); if (!reihe.includes(id) && REGISTER.some((w) => w.id === id)) reihe.push(id); }); REGISTER.forEach((w) => reihe.includes(w.id) || reihe.push(w.id)); return reihe.map((id) => { const w = REGISTER.find((x) => x.id === id); return { id, titel: w.titel, wo: w.wo, an: !aus.has(id) }; }); } /** Übernimmt einen geänderten Plan: speichern, nachladen, neu zeichnen. */ export function widgetPlanSetzen(plan) { state.settings.widgets = plan.map((p) => (p.an ? "" : AUS) + p.id).join(","); saveSettings(); widgetsZeichnen(); // Ein gerade erst eingeschaltetes Widget hat seine Daten noch nie geholt. widgetsLaden(); } /** * 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 an = new Set(widgetPlan().filter((p) => p.an).map((p) => p.id)); const dran = REGISTER.filter((w) => { if (!w.laden) return false; if (nurId) return w.id === nurId; return an.has(w.id) && !geladen.has(w.id); }); await Promise.all( dran.map(async (w) => { geladen.add(w.id); 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(() => { if (widgetPlan().find((p) => p.id === w.id)?.an) 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 = widgetPlan() .filter((p) => p.an) .map((p) => REGISTER.find((w) => w.id === p.id)) .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); }