Widget-Rahmen: Widgets sind Beschreibungen, keine Kaesten in der Seite
widgetAnmelden({ id, titel, wo, sichtbar, laden, leer, inhalt, takt }) —
der Rahmen macht daraus ein Fenster in der Spalte (Terminal) oder ein
Schild (Desktop), kennt lädt/fertig/nichts da/Fehler und haelt den
Nachladetakt. Benutzer, Wetter und Systemzustand sind umgestellt; aus
index.html sind vier feste Kaesten verschwunden und aus dem Raster die
Einzelplaetze.
Zwei Dinge, die erst dabei sichtbar wurden:
- Der Server meldete 'kein Ort eingestellt' und 'Dienst nicht
erreichbar' beide als 204. Ein Ausfall sah damit aus wie ein
abgeschaltetes Widget. Ausfall ist jetzt 502, mit Test.
- Die Reihenfolge haengt nicht mehr an der Modulauswertung: Seit
glyphs.js seine Raster mit await auf oberster Ebene laedt, laufen
Module ohne Symbole vorher durch — der Systemzustand stand deshalb
ueber dem Benutzer. widgetsOrdnen() macht die Reihenfolge zur Angabe,
und genau dort haengt spaeter die im Server gespeicherte ein.
This commit is contained in:
1 parent
981a2331df
commit
b7cc06b7c1
12 files changed
+461
-180
No files matched your search
@@ -0,0 +1,151 @@
|
||||
/* ── 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.
|
||||
*
|
||||
* Beide Container werden bei jedem Durchgang neu gefüllt statt einzeln
|
||||
* nachgeführt: Bei einer Handvoll Widgets ist das billiger als Buchführung
|
||||
* darüber, was sich geändert hat — und es kann nicht auseinanderlaufen.
|
||||
*/
|
||||
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");
|
||||
});
|
||||
|
||||
spalte.innerHTML = desktop
|
||||
? ""
|
||||
: dran
|
||||
.map(
|
||||
(w) => `
|
||||
<section class="win win-widget" data-widget="${w.id}">
|
||||
<div class="win-bar"><span class="win-title">${w.titel}</span><span class="win-dots">●●●</span></div>
|
||||
<div class="win-body">${zustandHtml(w)}</div>
|
||||
</section>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
schilder.innerHTML = desktop
|
||||
? dran.map((w) => `<div class="deskwidget" data-widget="${w.id}">${zustandHtml(w)}</div>`).join("")
|
||||
: "";
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { escapeHtml, state } from "../core.js";
|
||||
import { widgetAnmelden } from "./rahmen.js";
|
||||
|
||||
/* ── Systemzustand ─────────────────────────────────────────────────────────
|
||||
*
|
||||
* Nur im Terminal-Modus ein Widget. Auf dem Schreibtisch gibt es dieselben
|
||||
* Zahlen als Fenster über das Startmenü — ein Schild, das dauerhaft „modus:
|
||||
* desktop" behauptet, während man im Desktop-Modus steht, sagt niemandem etwas.
|
||||
*/
|
||||
export function statsHtml() {
|
||||
const up = Object.values(state.probes).filter((s) => s === "up").length;
|
||||
const rows = [
|
||||
["apps", `${state.cfg.apps.length}`],
|
||||
["erreichbar", `${up}/${state.cfg.apps.length}`],
|
||||
["modus", state.settings.mode],
|
||||
["röhre", state.settings.phosphor],
|
||||
["version", `v${state.cfg.version}`],
|
||||
];
|
||||
return `<dl class="statlist">${rows
|
||||
.map(([k, v]) => `<dt>${k}</dt><dd>${escapeHtml(v)}</dd>`)
|
||||
.join("")}</dl>`;
|
||||
}
|
||||
|
||||
widgetAnmelden({
|
||||
id: "system",
|
||||
titel: "system",
|
||||
wo: ["terminal"],
|
||||
inhalt: statsHtml,
|
||||
});
|
||||
+22
-27
@@ -1,5 +1,6 @@
|
||||
import { $, escapeAttr, state } from "../core.js";
|
||||
import { escapeAttr, escapeHtml, state } from "../core.js";
|
||||
import { glyphSvg } from "../glyphs.js";
|
||||
import { widgetAnmelden } from "./rahmen.js";
|
||||
|
||||
function greeting() {
|
||||
const h = new Date().getHours();
|
||||
@@ -15,30 +16,24 @@ function greeting() {
|
||||
* die HoneyBar in den Apps nicht hat — beide sollen aber gleich aussehen, sonst
|
||||
* merkt man beim Wechsel, dass es zwei verschiedene HoneyBars sind. Auf dem
|
||||
* Schreibtisch ist ohnehin Platz dafür, und die Begrüßung gehört dazu.
|
||||
*
|
||||
* Kein `laden`: Der Name steht schon im Zustand, seit die Seite ihn geholt hat.
|
||||
*/
|
||||
function userHtml() {
|
||||
const bild = state.me && state.me.picture
|
||||
? `<img class="uw-bild" src="${escapeAttr(state.me.picture)}" alt="" />`
|
||||
: `<span class="uw-bild uw-leer">${glyphSvg("chip")}</span>`;
|
||||
return `
|
||||
<div class="uw">
|
||||
${bild}
|
||||
<div class="uw-text">
|
||||
<b class="uw-name">${state.cfg.user}</b>
|
||||
<span class="uw-gruss">${greeting()}</span>
|
||||
<span class="uw-host">${state.cfg.user}@${state.cfg.hostname}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export function renderUser() {
|
||||
const tile = $("userTile");
|
||||
const desk = $("userDesk");
|
||||
const desktopMode = state.settings.mode === "desktop";
|
||||
const html = userHtml();
|
||||
|
||||
tile.hidden = desktopMode;
|
||||
desk.hidden = !desktopMode;
|
||||
tile.querySelector(".user-body").innerHTML = html;
|
||||
desk.innerHTML = html;
|
||||
}
|
||||
widgetAnmelden({
|
||||
id: "benutzer",
|
||||
titel: "angemeldet",
|
||||
inhalt() {
|
||||
const bild = state.me && state.me.picture
|
||||
? `<img class="uw-bild" src="${escapeAttr(state.me.picture)}" alt="" />`
|
||||
: `<span class="uw-bild uw-leer">${glyphSvg("chip")}</span>`;
|
||||
return `
|
||||
<div class="uw">
|
||||
${bild}
|
||||
<div class="uw-text">
|
||||
<b class="uw-name">${escapeHtml(state.cfg.user)}</b>
|
||||
<span class="uw-gruss">${greeting()}</span>
|
||||
<span class="uw-host">${escapeHtml(state.cfg.user)}@${escapeHtml(state.cfg.hostname)}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
},
|
||||
});
|
||||
+62
-44
@@ -1,5 +1,6 @@
|
||||
import { $, api, state } from "../core.js";
|
||||
import { api, $, state } from "../core.js";
|
||||
import { glyphSvg } from "../glyphs.js";
|
||||
import { widgetAnmelden, widgetsLaden } from "./rahmen.js";
|
||||
|
||||
/* ── Wetter ────────────────────────────────────────────────────────────────
|
||||
*
|
||||
@@ -7,52 +8,63 @@ import { glyphSvg } from "../glyphs.js";
|
||||
* was er hergibt. Kein Ort eingestellt oder Dienst nicht erreichbar: 204, und
|
||||
* das Widget bleibt weg. Eine Zahl von gestern wäre schlechter als keine.
|
||||
*/
|
||||
export async function loadWeather() {
|
||||
if (!state.settings.weatherCity) {
|
||||
state.weather = null;
|
||||
renderWeather();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
state.weather = await api("/api/weather");
|
||||
} catch {
|
||||
state.weather = null;
|
||||
}
|
||||
renderWeather();
|
||||
}
|
||||
widgetAnmelden({
|
||||
id: "wetter",
|
||||
titel: "wetter",
|
||||
// Der eigene Schalter — unabhängig von dem für die HoneyBar.
|
||||
sichtbar: () => state.settings.weatherDesk === "on",
|
||||
leer: "verbergen",
|
||||
takt: 10 * 60 * 1000, // derselbe Takt, in dem der Server seinen Speicher erneuert
|
||||
|
||||
function weatherHtml(w) {
|
||||
return `
|
||||
<div class="wx">
|
||||
<span class="wx-glyph">${glyphSvg(w.glyph)}</span>
|
||||
<div class="wx-text">
|
||||
<b class="wx-temp">${Math.round(w.grad)}°</b>
|
||||
<span class="wx-desc">${w.text}</span>
|
||||
<span class="wx-city">${w.ort}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
async laden() {
|
||||
if (!state.settings.weatherCity) {
|
||||
state.weather = null;
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
state.weather = await api("/api/weather");
|
||||
} catch (e) {
|
||||
// Auch im Fehlerfall den alten Stand wegwerfen: Das Widget zeigt dann
|
||||
// „nicht erreichbar", und in der Leiste verschwindet das Symbol. Eine
|
||||
// Temperatur von vorhin, die aussieht wie von jetzt, wäre schlechter.
|
||||
state.weather = null;
|
||||
throw e;
|
||||
}
|
||||
// 204: kein Ort, kein Dienst — der Aufrufhelfer gibt dann nichts zurück.
|
||||
if (!state.weather || !state.weather.glyph) {
|
||||
state.weather = null;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
export function renderWeather() {
|
||||
const w = state.weather;
|
||||
const tile = $("wxTile");
|
||||
const desk = $("wxDesk");
|
||||
inhalt() {
|
||||
const w = state.weather;
|
||||
return `
|
||||
<div class="wx">
|
||||
<span class="wx-glyph">${glyphSvg(w.glyph)}</span>
|
||||
<div class="wx-text">
|
||||
<b class="wx-temp">${Math.round(w.grad)}°</b>
|
||||
<span class="wx-desc">${w.text}</span>
|
||||
<span class="wx-city">${w.ort}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Und in der HoneyBar. Die Menüleiste hier ist zwar anderer Code als
|
||||
* honeybar.js in den Apps, aber für den, der davorsitzt, dieselbe HoneyBar —
|
||||
* ein Schalter, der nur woanders wirkt, wäre nicht zu erklären.
|
||||
*
|
||||
* Sie hängt am selben geladenen Stand, hat aber ihren eigenen Schalter und
|
||||
* gehört nicht in den Widget-Rahmen: Sie ist kein Widget, sondern ein Teil der
|
||||
* Leiste.
|
||||
*/
|
||||
export function renderWeatherBar() {
|
||||
const bar = $("mbWx");
|
||||
const desktopMode = state.settings.mode === "desktop";
|
||||
|
||||
// Das Widget: Fenster im Terminal-Modus, Schild auf dem Schreibtisch.
|
||||
const alsWidget = state.settings.weatherDesk === "on" && w;
|
||||
tile.hidden = !alsWidget || desktopMode;
|
||||
desk.hidden = !alsWidget || !desktopMode;
|
||||
if (alsWidget) {
|
||||
const html = weatherHtml(w);
|
||||
tile.querySelector(".wx-body").innerHTML = html;
|
||||
desk.innerHTML = html;
|
||||
}
|
||||
|
||||
// Und in der HoneyBar. Die Menüleiste hier ist zwar anderer Code als
|
||||
// honeybar.js in den Apps, aber für den, der davorsitzt, dieselbe HoneyBar —
|
||||
// ein Schalter, der nur woanders wirkt, wäre nicht zu erklären.
|
||||
if (!bar) return;
|
||||
const w = state.weather;
|
||||
const inBar = state.settings.weatherBar === "on" && w;
|
||||
bar.hidden = !inBar;
|
||||
if (inBar) {
|
||||
@@ -60,3 +72,9 @@ export function renderWeather() {
|
||||
bar.title = `${w.ort}: ${w.text}, ${Math.round(w.grad)}°`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Beides zusammen: Widget und Leiste hängen am selben Abruf. */
|
||||
export async function loadWeather() {
|
||||
await widgetsLaden("wetter");
|
||||
renderWeatherBar();
|
||||
}
|
||||
Reference in new issue
Block a user