Files
marc 3807ddca24 Notiz zeigt sich sofort gesetzt, Widgets nach Wunsch des Nutzers
Aus dem Zettel wird die Notiz: Der Text steht immer formatiert da, roh ist nur
die Zeile mit der Schreibmarke. Dazu eine kleine Werkzeugleiste (Aufgabe,
Aufzaehlung, Nummer, Ueberschrift, fett, Linie), Enter fuehrt Listen fort,
Backspace am Zeilenanfang nimmt erst den Vorsatz. Fuer groessere Umbauten
bleibt der Quelltext-Knopf.

Kein contenteditable: Der Quelltext bleibt die Wahrheit und wird nie aus dem
HTML zurueckgerechnet, das der Browser beim Tippen erzeugt.

Reihenfolge und An/Aus der Widgets liegen jetzt je Nutzer auf dem Server, in
einer Einstellung ("benutzer,notiz,-system,wetter,bild"). Der Schalter fuer das
Wetter auf dem Schreibtisch faellt damit weg — es ist ein Widget wie jedes
andere.

Die Einstellungen sind in Abschnitte sortiert (Darstellung, HoneyBar, Widgets,
Wetter, Programme, Konto), Beschriftung links, Schalter rechts.
2026-08-31 01:24:50 +02:00

250 lines
9.1 KiB
JavaScript

/* ── 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: () => "<html>"
* 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 `<p class="w-zustand">lädt …</p>`;
case "fehler":
return `<p class="w-zustand w-fehler">nicht erreichbar</p>`;
case "leer":
return `<p class="w-zustand">${w.leer}</p>`;
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 =
`<div class="win-bar"><span class="win-title">${w.titel}</span>` +
`<span class="win-dots">&#9679;&#9679;&#9679;</span></div><div class="win-body"></div>`;
}
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);
}