Zettel: ein Textfeld, mehr nicht — InkNote gibt es schon. Gelesen wird gesetzt (Ueberschriften, Haken, Aufzaehlungen, fett/kursiv/code, Strich), geschrieben wird roh. Haken lassen sich ohne Bearbeiten setzen und aendern die Quelle. Gespeichert wird 800 ms nach dem letzten Anschlag, sofort beim Verlassen und beim Schliessen des Tabs (keepalive statt sendBeacon: der kann nur POST und traegt keine Kopfzeilen). Grenze 4000 Zeichen, vom Server durchgesetzt. Bild: genau eines je Nutzer, hochgeladen oder verlinkt. Die erste Datei, die HoneyOS ablegt — in bilder/ neben honeyos.json, benannt nach einem Fingerabdruck der Nutzerkennung. Ein neues Bild ersetzt das alte samt Datei, die Adresse traegt einen Versionsanteil gegen den Zwischen- speicher. SVG kommt nicht herein (Text, kann Skripte tragen), und der Typ wird an den Bytes erkannt, nicht an der Angabe des Browsers. Der Rahmen kann jetzt Widgets mit eigenem DOM: 'stabil' haelt den Inhalt, 'montiert' verdrahtet ihn. Ohne das haette das Anklopfen bei den Apps alle zwei Minuten den Text unter den Fingern ausgetauscht. Nebenbei: Der Aufrufhelfer setzte bei jedem Schreibzugriff Content-Type: application/json — auch fuer Bildbytes.
189 lines
6.8 KiB
JavaScript
189 lines
6.8 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 { $, 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 `<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">●●●</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 = 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);
|
|
}
|