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
+434
-153
No files matched your search
@@ -165,51 +165,36 @@ ist am 29.08.2026 weggefallen: Aether und InkNote liegen längst unter
|
|||||||
erst nach der Sitzung. **Die HoneyBar weiß dort jetzt, wer schaut.** Was bleibt,
|
erst nach der Sitzung. **Die HoneyBar weiß dort jetzt, wer schaut.** Was bleibt,
|
||||||
ist die Frage, ob es sich bei einem Nutzer lohnt.
|
ist die Frage, ob es sich bei einem Nutzer lohnt.
|
||||||
|
|
||||||
### 1.4 HoneyOS selbst als PWA
|
### 1.4 Widget-Rahmen — ✅ gebaut (30.08.2026)
|
||||||
|
|
||||||
Heute ist HoneyOS eine Website: Es hat `theme-color`, aber kein Manifest, kein
|
Ein Widget ist jetzt eine **Beschreibung**, kein Stück HTML im Dokument:
|
||||||
Symbol und keinen Service Worker — installierbar ist es damit nicht. Am Handy
|
|
||||||
landet man also im Browser mit Adressleiste, während Aether und InkNote als
|
|
||||||
eigene Apps danebenliegen. Das ist genau der Bruch, den das Ganze eigentlich
|
|
||||||
verhindern soll: Der Schreibtisch sollte sich am wenigsten nach Website
|
|
||||||
anfühlen.
|
|
||||||
|
|
||||||
Was dazugehört, klein bis groß:
|
```js
|
||||||
|
widgetAnmelden({
|
||||||
|
id, titel,
|
||||||
|
wo: ["terminal", "desktop"], // wo es erscheinen darf
|
||||||
|
sichtbar: () => …, // 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, // Nachladen, optional
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
1. **Manifest** — Name, Startadresse, `display: standalone`, Hintergrund- und
|
Der Rahmen (`web/js/widgets/rahmen.js`) macht daraus im Terminal-Modus ein
|
||||||
Themenfarbe. Die Farben sollten der eingestellten Röhre folgen; sie liegen
|
Fenster in der Widgetspalte und im Desktop-Modus ein Schild, kennt die vier
|
||||||
im Server, das Manifest kann also erzeugt statt fest hinterlegt werden
|
Zustände (lädt · fertig · nichts da · Fehler) und hält den Nachladetakt. Ein
|
||||||
(Aether macht das beim Symbol schon so).
|
Widget, das seinen Server nicht erreicht, sagt das **in seinem eigenen Rahmen**
|
||||||
2. **Symbol** — ein Pixelglyph in 192 und 512 px, im Amber-Look. Passt zum
|
und reißt weder den Schreibtisch noch die anderen Widgets mit.
|
||||||
Rest und ist ohne fremde Werkzeuge zu machen.
|
|
||||||
3. **Service Worker** — nötig für die Installierbarkeit. Hier ist zu
|
|
||||||
entscheiden, was er überhaupt soll (siehe unten).
|
|
||||||
|
|
||||||
**Die eigentliche Frage ist der Offline-Fall.** HoneyOS zeigt fremde Apps und
|
Umgestellt sind Benutzer, Wetter und der Systemzustand. Aus der `index.html`
|
||||||
später deren Daten — ohne Netz gibt es davon nichts. Ein Schreibtisch, der
|
sind damit vier fest verdrahtete Kästen verschwunden, und aus dem Raster die
|
||||||
offline mit leeren Kacheln dasteht, ist schlimmer als ein ehrlicher Hinweis.
|
Einzelplätze — vorher hätte das dritte Widget beides wieder angefasst.
|
||||||
Sinnvoll wäre: Oberfläche und App-Liste im Cache, damit man die Symbole sieht
|
|
||||||
und antippen kann, und ein deutlicher Vermerk „ohne Verbindung". Widgets
|
|
||||||
bleiben leer, statt Altes von gestern als aktuell auszugeben.
|
|
||||||
|
|
||||||
**Aus dem Aether-Debakel gelernt und hier von Anfang an zu beachten:** Ein
|
**Was dabei offen gelassen wurde:** die dritte Form. Widgets sollen später auch
|
||||||
Eintrag im Cache ist eine ganze Antwort **samt Kopfzeilen**. Wird die Startseite
|
als Aufklappfeld in der HoneyBar leben können, deshalb erzeugt `inhalt()` nur
|
||||||
vorab abgelegt, friert damit auch ihre CSP ein — und eine Änderung an der
|
den Kern; die Verpackung entsteht im Rahmen. Und die Reihenfolge ist eine
|
||||||
Konfiguration erreicht den Browser nie wieder. Also Navigationen Netz zuerst,
|
Liste — genau das, was „Widgets verschieben" braucht.
|
||||||
Cache nur als Rückfall.
|
|
||||||
|
|
||||||
**Reihenfolge:** Erst umziehen (siehe unten), dann installierbar machen. Ein
|
|
||||||
Ursprungswechsel macht aus einer installierten PWA eine fremde App — wer
|
|
||||||
vorher installiert, installiert zweimal.
|
|
||||||
|
|
||||||
### 1.5 Widget-Rahmen
|
|
||||||
|
|
||||||
Ein Fenstertyp, der einen Inhalt holt, ihn anzeigt und sich benimmt, wenn es
|
|
||||||
schiefgeht: lädt gerade, nichts da, nicht angemeldet, Fehler. Einmal richtig
|
|
||||||
gebaut, kostet jedes weitere Widget danach fast nichts.
|
|
||||||
|
|
||||||
Muss in **beiden Betriebsarten** funktionieren — im Desktop als Fenster, im
|
|
||||||
Terminal als Kachel.
|
|
||||||
|
|
||||||
## 2 · Erste Widgets
|
## 2 · Erste Widgets
|
||||||
|
|
||||||
@@ -424,7 +409,7 @@ schon gebaut) oder am Handy Pfeiltasten statt Ziehen anbieten. Nicht vergessen:
|
|||||||
Ohne das ist die Funktion am Handy schlicht nicht da.
|
Ohne das ist die Funktion am Handy schlicht nicht da.
|
||||||
|
|
||||||
Voraussetzung ist ein Widget-Verzeichnis — heute stehen Benutzer und Wetter
|
Voraussetzung ist ein Widget-Verzeichnis — heute stehen Benutzer und Wetter
|
||||||
fest im HTML. Sinnvoll zusammen mit dem Widget-Rahmen (1.5) zu bauen, nicht
|
fest im HTML. Sinnvoll zusammen mit dem Widget-Rahmen (1.4) zu bauen, nicht
|
||||||
davor.
|
davor.
|
||||||
|
|
||||||
### Widgets in der HoneyBar selbst
|
### Widgets in der HoneyBar selbst
|
||||||
@@ -434,9 +419,9 @@ ist aber die Seite, die man am seltensten offen hat — die HoneyBar dagegen ist
|
|||||||
überall. Naheliegend also: ein Knopf in der HoneyBar klappt dasselbe Widget als
|
überall. Naheliegend also: ein Knopf in der HoneyBar klappt dasselbe Widget als
|
||||||
kleines Feld auf, ohne die App zu verlassen.
|
kleines Feld auf, ohne die App zu verlassen.
|
||||||
|
|
||||||
Technisch ist das dieselbe Sache wie 1.5, nur ein zweites Zuhause für denselben
|
Technisch ist das dieselbe Sache wie 1.4, nur ein zweites Zuhause für denselben
|
||||||
Inhalt: Der Rahmen muss dann drei Formen können — Fenster, Kachel, Aufklappfeld.
|
Inhalt: Der Rahmen muss dann drei Formen können — Fenster, Kachel, Aufklappfeld.
|
||||||
Das ist beim **Bauen** von 1.5 zu bedenken, hinterher wird es teuer.
|
Das ist beim **Bauen** von 1.4 zu bedenken, hinterher wird es teuer.
|
||||||
|
|
||||||
Für Widgets, die nur sich selbst brauchen (Wetter, Bild, Uhr), ist das
|
Für Widgets, die nur sich selbst brauchen (Wetter, Bild, Uhr), ist das
|
||||||
geschenkt — sie fragen HoneyOS, nicht die App.
|
geschenkt — sie fragen HoneyOS, nicht die App.
|
||||||
@@ -445,6 +430,48 @@ geschenkt — sie fragen HoneyOS, nicht die App.
|
|||||||
|
|
||||||
## 3 · Ausbau
|
## 3 · Ausbau
|
||||||
|
|
||||||
|
### 3.x HoneyOS selbst als PWA — zurückgestellt (30.08.2026)
|
||||||
|
|
||||||
|
Heute ist HoneyOS eine Website: Es hat `theme-color`, aber kein Manifest, kein
|
||||||
|
Symbol und keinen Service Worker — installierbar ist es damit nicht. Am Handy
|
||||||
|
landet man also im Browser mit Adressleiste, während Aether und InkNote als
|
||||||
|
eigene Apps danebenliegen. Das ist genau der Bruch, den das Ganze eigentlich
|
||||||
|
verhindern soll: Der Schreibtisch sollte sich am wenigsten nach Website
|
||||||
|
anfühlen.
|
||||||
|
|
||||||
|
Was dazugehört, klein bis groß:
|
||||||
|
|
||||||
|
1. **Manifest** — Name, Startadresse, `display: standalone`, Hintergrund- und
|
||||||
|
Themenfarbe. Die Farben sollten der eingestellten Röhre folgen; sie liegen
|
||||||
|
im Server, das Manifest kann also erzeugt statt fest hinterlegt werden
|
||||||
|
(Aether macht das beim Symbol schon so).
|
||||||
|
2. **Symbol** — ein Pixelglyph in 192 und 512 px, im Amber-Look. Passt zum
|
||||||
|
Rest und ist ohne fremde Werkzeuge zu machen.
|
||||||
|
3. **Service Worker** — nötig für die Installierbarkeit. Hier ist zu
|
||||||
|
entscheiden, was er überhaupt soll (siehe unten).
|
||||||
|
|
||||||
|
**Die eigentliche Frage ist der Offline-Fall.** HoneyOS zeigt fremde Apps und
|
||||||
|
später deren Daten — ohne Netz gibt es davon nichts. Ein Schreibtisch, der
|
||||||
|
offline mit leeren Kacheln dasteht, ist schlimmer als ein ehrlicher Hinweis.
|
||||||
|
Sinnvoll wäre: Oberfläche und App-Liste im Cache, damit man die Symbole sieht
|
||||||
|
und antippen kann, und ein deutlicher Vermerk „ohne Verbindung". Widgets
|
||||||
|
bleiben leer, statt Altes von gestern als aktuell auszugeben.
|
||||||
|
|
||||||
|
**Aus dem Aether-Debakel gelernt und hier von Anfang an zu beachten:** Ein
|
||||||
|
Eintrag im Cache ist eine ganze Antwort **samt Kopfzeilen**. Wird die Startseite
|
||||||
|
vorab abgelegt, friert damit auch ihre CSP ein — und eine Änderung an der
|
||||||
|
Konfiguration erreicht den Browser nie wieder. Also Navigationen Netz zuerst,
|
||||||
|
Cache nur als Rückfall.
|
||||||
|
|
||||||
|
**Zurückgestellt (Marc, 30.08.2026): erst die Hauptfunktionen, dann das
|
||||||
|
Gehäuse.** Die PWA ist mehr Gefühl als Funktion — sie macht nichts möglich, was
|
||||||
|
heute nicht ginge, während der Widget-Rahmen darunter alles Weitere trägt.
|
||||||
|
|
||||||
|
**Reihenfolge:** Erst umziehen (siehe unten), dann installierbar machen. Ein
|
||||||
|
Ursprungswechsel macht aus einer installierten PWA eine fremde App — wer
|
||||||
|
vorher installiert, installiert zweimal.
|
||||||
|
|
||||||
|
|
||||||
- Welche Widgets wo liegen, **pro Nutzer im Server** merken — die Einstellungen
|
- Welche Widgets wo liegen, **pro Nutzer im Server** merken — die Einstellungen
|
||||||
liegen ohnehin schon dort, das ist eine Erweiterung, kein Neubau
|
liegen ohnehin schon dort, das ist eine Erweiterung, kein Neubau
|
||||||
- Widgets im Terminal-Modus
|
- Widgets im Terminal-Modus
|
||||||
|
|||||||
@@ -184,6 +184,36 @@ in beiden: Menüleiste, Palette, Einstellungen, App-Liste.
|
|||||||
**Tasten:** `M` Betriebsart · `S` Einstellungen · `?` Hilfe ·
|
**Tasten:** `M` Betriebsart · `S` Einstellungen · `?` Hilfe ·
|
||||||
`Esc` schließt Startmenü, oberstes Fenster oder Dialog — in dieser Reihenfolge.
|
`Esc` schließt Startmenü, oberstes Fenster oder Dialog — in dieser Reihenfolge.
|
||||||
|
|
||||||
|
## Widgets
|
||||||
|
|
||||||
|
Ein Widget ist eine **Beschreibung**, kein Kasten in der `index.html`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
widgetAnmelden({
|
||||||
|
id: "wetter", titel: "wetter",
|
||||||
|
wo: ["terminal", "desktop"], // wo es erscheinen darf
|
||||||
|
sichtbar: () => state.settings.weatherDesk === "on",
|
||||||
|
laden: async () => …, // false = es gibt nichts zu zeigen
|
||||||
|
leer: "verbergen", // oder ein Satz, der dann dasteht
|
||||||
|
inhalt: () => "<html>",
|
||||||
|
takt: 10 * 60 * 1000, // Nachladen, optional
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Den Rest macht `web/js/widgets/rahmen.js`: Im Terminal-Modus wird daraus ein
|
||||||
|
Fenster in der linken Spalte, im Desktop-Modus ein Schild oben rechts. Er kennt
|
||||||
|
vier Zustände — **lädt · fertig · nichts da · Fehler** — und hält den
|
||||||
|
Nachladetakt.
|
||||||
|
|
||||||
|
**Ein Widget, das seinen Server nicht erreicht, sagt das in seinem eigenen
|
||||||
|
Rahmen.** Es verschwindet nicht (das sähe aus wie abgeschaltet) und reißt weder
|
||||||
|
den Schreibtisch noch die anderen Widgets mit. Wo „nichts da" schlicht heißt,
|
||||||
|
dass es nichts zu zeigen gibt, kann ein Widget mit `leer: "verbergen"` ganz
|
||||||
|
wegbleiben — so macht es das Wetter ohne eingestellten Ort.
|
||||||
|
|
||||||
|
Eine neue Anzeige ist damit eine Datei in `web/js/widgets/` und ein Import in
|
||||||
|
`os.js`. Weder das Raster noch die Seite werden dafür angefasst.
|
||||||
|
|
||||||
## Wetter
|
## Wetter
|
||||||
|
|
||||||
Ein Pixelsymbol mit der Temperatur — wahlweise auf dem Schreibtisch, in der
|
Ein Pixelsymbol mit der Temperatur — wahlweise auf dem Schreibtisch, in der
|
||||||
|
|||||||
@@ -15,8 +15,22 @@ import (
|
|||||||
// Open-Meteo an — das wäre langsam, unzuverlässig und benutzte einen fremden
|
// Open-Meteo an — das wäre langsam, unzuverlässig und benutzte einen fremden
|
||||||
// Dienst für unsere Testläufe.
|
// Dienst für unsere Testläufe.
|
||||||
func FakeFuerTest(t *testing.T, code int, grad float64) *Dienst {
|
func FakeFuerTest(t *testing.T, code int, grad float64) *Dienst {
|
||||||
|
return fake(t, code, grad, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KaputtFuerTest stellt einen Dienst, der nicht antwortet — für den Fall, den
|
||||||
|
// die Oberfläche seit dem Widget-Rahmen anders behandelt als „nichts da".
|
||||||
|
func KaputtFuerTest(t *testing.T) *Dienst {
|
||||||
|
return fake(t, 0, 0, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fake(t *testing.T, code int, grad float64, kaputt bool) *Dienst {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if kaputt {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
if strings.Contains(r.URL.Path, "search") {
|
if strings.Contains(r.URL.Path, "search") {
|
||||||
json.NewEncoder(w).Encode(map[string]any{"results": []map[string]any{
|
json.NewEncoder(w).Encode(map[string]any{"results": []map[string]any{
|
||||||
{"name": "Köln", "country": "Deutschland", "admin1": "Nordrhein-Westfalen",
|
{"name": "Köln", "country": "Deutschland", "admin1": "Nordrhein-Westfalen",
|
||||||
|
|||||||
@@ -380,7 +380,12 @@ func (s *Server) handleWeather(w http.ResponseWriter, r *http.Request, sub strin
|
|||||||
wetter, err := s.wetter.Jetzt(r.Context(), set["weatherCity"], lat, lon)
|
wetter, err := s.wetter.Jetzt(r.Context(), set["weatherCity"], lat, lon)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("wetter: %v", err)
|
log.Printf("wetter: %v", err)
|
||||||
w.WriteHeader(http.StatusNoContent)
|
// 502, nicht 204: Ein Ort ist eingestellt, es ist also etwas zu zeigen —
|
||||||
|
// der Dienst ist nur gerade nicht da. Die Oberflaeche unterscheidet das
|
||||||
|
// seit dem Widget-Rahmen: „nichts eingestellt" verschwindet still,
|
||||||
|
// „nicht erreichbar" sagt es. Beides als 204 zu melden hiess, dass ein
|
||||||
|
// Ausfall aussah wie ein abgeschaltetes Widget.
|
||||||
|
writeJSON(w, http.StatusBadGateway, map[string]string{"error": "Wetterdienst nicht erreichbar"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, wetter)
|
writeJSON(w, http.StatusOK, wetter)
|
||||||
|
|||||||
+25
-16
@@ -284,14 +284,11 @@ body {
|
|||||||
grid-template-columns: 320px minmax(0, 1fr);
|
grid-template-columns: 320px minmax(0, 1fr);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
/* Ohne feste Plätze verteilt das Raster die drei Bereiche der Reihe nach —
|
/* Zwei Bereiche, keine Einzelplätze mehr: Links die Widgetspalte, rechts
|
||||||
das Wetter landete dann neben „system" und schob die Kacheln in die
|
die Apps. Vorher hatte jedes Widget seinen festen Platz im Raster — beim
|
||||||
schmale linke Spalte. Links stehen die Anzeigen untereinander, rechts
|
dritten hätte man das Raster wieder anfassen müssen. */
|
||||||
gehören die Apps, über die volle Höhe. */
|
.widgetspalte { grid-column: 1; }
|
||||||
.win-user { grid-column: 1; grid-row: 1; }
|
.tiles { grid-column: 2; }
|
||||||
.win-status { grid-column: 1; grid-row: 2; }
|
|
||||||
.win-weather { grid-column: 1; grid-row: 3; }
|
|
||||||
.tiles { grid-column: 2; grid-row: 1 / span 3; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fenster */
|
/* Fenster */
|
||||||
@@ -1028,9 +1025,7 @@ body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Terminal-Modus: ein Fenster wie die anderen. Schmal gehalten — über die
|
|
||||||
volle Breite gezogen sähe eine Temperatur nach Bekanntmachung aus. */
|
|
||||||
.win-weather { max-width: 320px; align-self: start; }
|
|
||||||
|
|
||||||
|
|
||||||
/* Schreibtisch-Modus: schwebendes Schild, rechts unter der Menüleiste. Nicht
|
/* Schreibtisch-Modus: schwebendes Schild, rechts unter der Menüleiste. Nicht
|
||||||
@@ -1101,16 +1096,30 @@ body {
|
|||||||
.uw-name { color: var(--p); font-size: 15px; }
|
.uw-name { color: var(--p); font-size: 15px; }
|
||||||
.uw-gruss { color: var(--p-mid); }
|
.uw-gruss { color: var(--p-mid); }
|
||||||
.uw-host { color: var(--p-dim); font-size: 12px; }
|
.uw-host { color: var(--p-dim); font-size: 12px; }
|
||||||
.win-user { max-width: 320px; align-self: start; }
|
/* Die Spalte selbst hält die Breite — vorher trug sie jedes Widget einzeln,
|
||||||
|
und ein neues ohne diese Zeile stand quer. */
|
||||||
|
.widgetspalte {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: clamp(14px, 3vw, 28px);
|
||||||
|
align-self: start;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
@media (min-width: 860px) {
|
||||||
|
.widgetspalte { max-width: 320px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Die vier Zustände, die der Rahmen kennt. „lädt" und „nicht erreichbar"
|
||||||
|
stehen im selben Kasten wie der Inhalt — ein Widget, das bei einem Fehler
|
||||||
|
verschwindet, sieht aus wie eines, das man nie eingeschaltet hat. */
|
||||||
|
.w-zustand { color: var(--p-dim); margin: 0; font-size: 12.5px; }
|
||||||
|
.w-fehler { color: var(--p-mid); }
|
||||||
|
|
||||||
/* Einspaltig (Handy, schmales Fenster) sollen die Widgets so breit sein wie das
|
/* Einspaltig (Handy, schmales Fenster) sollen die Widgets so breit sein wie das
|
||||||
Systemfenster. Drei verschieden breite Kästen untereinander sehen aus wie ein
|
Systemfenster. Drei verschieden breite Kästen untereinander sehen aus wie ein
|
||||||
Fehler, nicht wie Absicht. Muss hinter den Regeln stehen, die es aufhebt —
|
Fehler, nicht wie Absicht. Muss hinter den Regeln stehen, die es aufhebt —
|
||||||
eine Medienabfrage erhöht die Spezifität nicht. */
|
eine Medienabfrage erhöht die Spezifität nicht. */
|
||||||
@media (max-width: 859px) {
|
|
||||||
.win-weather,
|
|
||||||
.win-user { max-width: none; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Die Schilder auf dem Schreibtisch stapeln sich oben rechts. */
|
/* Die Schilder auf dem Schreibtisch stapeln sich oben rechts. */
|
||||||
.deskwidgets {
|
.deskwidgets {
|
||||||
|
|||||||
+6
-29
@@ -47,30 +47,10 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="stage" id="stage">
|
<main class="stage" id="stage">
|
||||||
<!-- Statusfenster -->
|
<!-- Links die Widgets untereinander, rechts die Apps. Was hier steht,
|
||||||
<section class="win win-status">
|
entscheidet der Widget-Rahmen (js/widgets/rahmen.js) — im
|
||||||
<div class="win-bar"><span class="win-title">system</span><span class="win-dots">●●●</span></div>
|
Dokument steht nur der Platz dafür. -->
|
||||||
<div class="win-body">
|
<div class="widgetspalte" id="widgetSpalte"></div>
|
||||||
<dl class="statlist" id="statList"></dl>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Wer angemeldet ist. Stand früher in der Menüleiste — dort ist
|
|
||||||
es aber der einzige Unterschied zur HoneyBar in den Apps
|
|
||||||
gewesen, und der Schreibtisch hat für so etwas Platz. -->
|
|
||||||
<section class="win win-user" id="userTile" hidden>
|
|
||||||
<div class="win-bar"><span class="win-title">angemeldet</span><span class="win-dots">●●●</span></div>
|
|
||||||
<div class="win-body user-body"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Wetter. Steht in beiden Betriebsarten zur Verfügung, wird aber
|
|
||||||
nur gezeigt, wenn ein Ort eingestellt und das Widget an ist. -->
|
|
||||||
<section class="win win-weather" id="wxTile" hidden>
|
|
||||||
<div class="win-bar"><span class="win-title">wetter</span><span class="win-dots">●●●</span></div>
|
|
||||||
<div class="win-body wx-body"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- App-Kacheln -->
|
|
||||||
<section class="tiles" id="tiles" aria-label="Apps starten"></section>
|
<section class="tiles" id="tiles" aria-label="Apps starten"></section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -78,11 +58,8 @@
|
|||||||
<div class="deskspace" id="deskspace" hidden>
|
<div class="deskspace" id="deskspace" hidden>
|
||||||
<div class="desk-icons" id="deskIcons"></div>
|
<div class="desk-icons" id="deskIcons"></div>
|
||||||
<div class="windows" id="windows"></div>
|
<div class="windows" id="windows"></div>
|
||||||
<!-- Dasselbe Wetter, im Schreibtisch-Modus als kleines Feld oben rechts -->
|
<!-- Dieselben Widgets, im Schreibtisch-Modus als Schilder oben rechts -->
|
||||||
<div class="deskwidgets">
|
<div class="deskwidgets" id="deskwidgets"></div>
|
||||||
<div class="deskwidget" id="userDesk" hidden></div>
|
|
||||||
<div class="deskwidget" id="wxDesk" hidden></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="taskbar" id="taskbar" hidden>
|
<footer class="taskbar" id="taskbar" hidden>
|
||||||
|
|||||||
+25
-20
@@ -4,8 +4,16 @@ import {
|
|||||||
SS_BOOTED, state,
|
SS_BOOTED, state,
|
||||||
} from "./core.js";
|
} from "./core.js";
|
||||||
import { GLYPHS, glyphSvg } from "./glyphs.js";
|
import { GLYPHS, glyphSvg } from "./glyphs.js";
|
||||||
import { renderUser } from "./widgets/user.js";
|
import { widgetsLaden, widgetsOrdnen, widgetsStarten, widgetsZeichnen } from "./widgets/rahmen.js";
|
||||||
import { loadWeather, renderWeather } from "./widgets/weather.js";
|
import "./widgets/user.js";
|
||||||
|
import { statsHtml } from "./widgets/system.js";
|
||||||
|
import { loadWeather, renderWeatherBar } from "./widgets/weather.js";
|
||||||
|
|
||||||
|
// Die Reihenfolge steht hier und nicht in den Importzeilen darüber: Module
|
||||||
|
// werden nicht in deren Reihenfolge ausgewertet, sobald eines auf oberster
|
||||||
|
// Ebene wartet (glyphs.js holt seine Raster mit await). Bis die Reihenfolge
|
||||||
|
// aus dem Server kommt, ist das ihre Stelle.
|
||||||
|
widgetsOrdnen(["benutzer", "system", "wetter"]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* HoneyOS — Desktop-Logik.
|
* HoneyOS — Desktop-Logik.
|
||||||
@@ -116,8 +124,11 @@ function renderShell() {
|
|||||||
// Nicht abgewartet: Das Wetter kommt von außen und darf den Aufbau des
|
// Nicht abgewartet: Das Wetter kommt von außen und darf den Aufbau des
|
||||||
// Schreibtischs nicht verzögern. Danach alle 10 Minuten, im selben Takt,
|
// Schreibtischs nicht verzögern. Danach alle 10 Minuten, im selben Takt,
|
||||||
// in dem der Server seinen Zwischenspeicher erneuert.
|
// in dem der Server seinen Zwischenspeicher erneuert.
|
||||||
loadWeather();
|
// Nicht abgewartet: Die Widgets holen ihre Daten von außen und dürfen den
|
||||||
setInterval(loadWeather, 10 * 60 * 1000);
|
// Aufbau des Schreibtischs nicht verzögern. Den Takt fürs Nachladen kennt
|
||||||
|
// jedes Widget selbst.
|
||||||
|
widgetsLaden().then(renderWeatherBar);
|
||||||
|
widgetsStarten();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMenubarApps() {
|
function renderMenubarApps() {
|
||||||
@@ -163,9 +174,7 @@ function renderMode() {
|
|||||||
closeAllWindows();
|
closeAllWindows();
|
||||||
renderTiles();
|
renderTiles();
|
||||||
}
|
}
|
||||||
renderStats();
|
widgetsZeichnen();
|
||||||
renderUser();
|
|
||||||
renderWeather();
|
|
||||||
paintProbes();
|
paintProbes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,18 +197,13 @@ function launch(app) {
|
|||||||
|
|
||||||
/** Die Kennzahlen stehen in beiden Ansichten — deshalb über die Klasse in
|
/** Die Kennzahlen stehen in beiden Ansichten — deshalb über die Klasse in
|
||||||
* alle vorhandenen Listen schreiben, nicht in eine bestimmte id. */
|
* alle vorhandenen Listen schreiben, nicht in eine bestimmte id. */
|
||||||
|
/** Zeichnet den Systemzustand überall neu, wo er gerade steht: als Widget in
|
||||||
|
* der Spalte und, im Desktop-Modus, im Fenster aus dem Startmenü. */
|
||||||
function renderStats() {
|
function renderStats() {
|
||||||
const up = Object.values(state.probes).filter((s) => s === "up").length;
|
widgetsZeichnen();
|
||||||
const rows = [
|
document.querySelectorAll("#windows .statlist").forEach((dl) => {
|
||||||
["apps", `${state.cfg.apps.length}`],
|
dl.outerHTML = statsHtml();
|
||||||
["erreichbar", `${up}/${state.cfg.apps.length}`],
|
});
|
||||||
["modus", state.settings.mode],
|
|
||||||
["röhre", state.settings.phosphor],
|
|
||||||
["version", `v${state.cfg.version}`],
|
|
||||||
];
|
|
||||||
const html = rows.map(([k, v]) => `<dt>${k}</dt><dd>${escapeHtml(v)}</dd>`).join("");
|
|
||||||
document.querySelectorAll(".statlist").forEach((dl) => (dl.innerHTML = html));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function tickClock() {
|
function tickClock() {
|
||||||
@@ -490,8 +494,9 @@ function openSystemWindow() {
|
|||||||
id: "system",
|
id: "system",
|
||||||
title: "system",
|
title: "system",
|
||||||
w: 320,
|
w: 320,
|
||||||
body: `<p class="greet"></p><dl class="statlist"></dl>`,
|
// Derselbe Inhalt wie das Widget im Terminal-Modus — eine Quelle, damit
|
||||||
onMount: renderStats,
|
// nicht zwei Listen mit denselben Zahlen auseinanderlaufen.
|
||||||
|
body: statsHtml(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
+12
-17
@@ -1,5 +1,6 @@
|
|||||||
import { $, escapeAttr, state } from "../core.js";
|
import { escapeAttr, escapeHtml, state } from "../core.js";
|
||||||
import { glyphSvg } from "../glyphs.js";
|
import { glyphSvg } from "../glyphs.js";
|
||||||
|
import { widgetAnmelden } from "./rahmen.js";
|
||||||
|
|
||||||
function greeting() {
|
function greeting() {
|
||||||
const h = new Date().getHours();
|
const h = new Date().getHours();
|
||||||
@@ -15,8 +16,13 @@ function greeting() {
|
|||||||
* die HoneyBar in den Apps nicht hat — beide sollen aber gleich aussehen, sonst
|
* 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
|
* 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.
|
* 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() {
|
widgetAnmelden({
|
||||||
|
id: "benutzer",
|
||||||
|
titel: "angemeldet",
|
||||||
|
inhalt() {
|
||||||
const bild = state.me && state.me.picture
|
const bild = state.me && state.me.picture
|
||||||
? `<img class="uw-bild" src="${escapeAttr(state.me.picture)}" alt="" />`
|
? `<img class="uw-bild" src="${escapeAttr(state.me.picture)}" alt="" />`
|
||||||
: `<span class="uw-bild uw-leer">${glyphSvg("chip")}</span>`;
|
: `<span class="uw-bild uw-leer">${glyphSvg("chip")}</span>`;
|
||||||
@@ -24,21 +30,10 @@ function userHtml() {
|
|||||||
<div class="uw">
|
<div class="uw">
|
||||||
${bild}
|
${bild}
|
||||||
<div class="uw-text">
|
<div class="uw-text">
|
||||||
<b class="uw-name">${state.cfg.user}</b>
|
<b class="uw-name">${escapeHtml(state.cfg.user)}</b>
|
||||||
<span class="uw-gruss">${greeting()}</span>
|
<span class="uw-gruss">${greeting()}</span>
|
||||||
<span class="uw-host">${state.cfg.user}@${state.cfg.hostname}</span>
|
<span class="uw-host">${escapeHtml(state.cfg.user)}@${escapeHtml(state.cfg.hostname)}</span>
|
||||||
</div>
|
</div>
|
||||||
</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;
|
|
||||||
}
|
|
||||||
+45
-27
@@ -1,5 +1,6 @@
|
|||||||
import { $, api, state } from "../core.js";
|
import { api, $, state } from "../core.js";
|
||||||
import { glyphSvg } from "../glyphs.js";
|
import { glyphSvg } from "../glyphs.js";
|
||||||
|
import { widgetAnmelden, widgetsLaden } from "./rahmen.js";
|
||||||
|
|
||||||
/* ── Wetter ────────────────────────────────────────────────────────────────
|
/* ── Wetter ────────────────────────────────────────────────────────────────
|
||||||
*
|
*
|
||||||
@@ -7,21 +8,38 @@ import { glyphSvg } from "../glyphs.js";
|
|||||||
* was er hergibt. Kein Ort eingestellt oder Dienst nicht erreichbar: 204, und
|
* 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.
|
* das Widget bleibt weg. Eine Zahl von gestern wäre schlechter als keine.
|
||||||
*/
|
*/
|
||||||
export async function loadWeather() {
|
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
|
||||||
|
|
||||||
|
async laden() {
|
||||||
if (!state.settings.weatherCity) {
|
if (!state.settings.weatherCity) {
|
||||||
state.weather = null;
|
state.weather = null;
|
||||||
renderWeather();
|
return false;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
state.weather = await api("/api/weather");
|
state.weather = await api("/api/weather");
|
||||||
} catch {
|
} 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;
|
state.weather = null;
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
renderWeather();
|
// 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;
|
||||||
|
},
|
||||||
|
|
||||||
function weatherHtml(w) {
|
inhalt() {
|
||||||
|
const w = state.weather;
|
||||||
return `
|
return `
|
||||||
<div class="wx">
|
<div class="wx">
|
||||||
<span class="wx-glyph">${glyphSvg(w.glyph)}</span>
|
<span class="wx-glyph">${glyphSvg(w.glyph)}</span>
|
||||||
@@ -31,28 +49,22 @@ function weatherHtml(w) {
|
|||||||
<span class="wx-city">${w.ort}</span>
|
<span class="wx-city">${w.ort}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export function renderWeather() {
|
/**
|
||||||
const w = state.weather;
|
* Und in der HoneyBar. Die Menüleiste hier ist zwar anderer Code als
|
||||||
const tile = $("wxTile");
|
* honeybar.js in den Apps, aber für den, der davorsitzt, dieselbe HoneyBar —
|
||||||
const desk = $("wxDesk");
|
* 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 bar = $("mbWx");
|
||||||
const desktopMode = state.settings.mode === "desktop";
|
if (!bar) return;
|
||||||
|
const w = state.weather;
|
||||||
// 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.
|
|
||||||
const inBar = state.settings.weatherBar === "on" && w;
|
const inBar = state.settings.weatherBar === "on" && w;
|
||||||
bar.hidden = !inBar;
|
bar.hidden = !inBar;
|
||||||
if (inBar) {
|
if (inBar) {
|
||||||
@@ -60,3 +72,9 @@ export function renderWeather() {
|
|||||||
bar.title = `${w.ort}: ${w.text}, ${Math.round(w.grad)}°`;
|
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();
|
||||||
|
}
|
||||||
@@ -46,6 +46,27 @@ func TestMitOrtKommtDasWetter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ist ein Ort eingestellt und der Dienst fällt aus, ist das etwas anderes als
|
||||||
|
// „nichts eingestellt": Es gibt etwas zu zeigen, es kommt nur gerade nicht. Die
|
||||||
|
// Oberfläche unterscheidet das — 204 verschwindet still, 502 sagt „nicht
|
||||||
|
// erreichbar". Beides als 204 zu melden hiess: Ein Ausfall sah aus wie ein
|
||||||
|
// abgeschaltetes Widget.
|
||||||
|
func TestAusfallIstNichtDasselbeWieNichts(t *testing.T) {
|
||||||
|
s := newTestServer(t, "")
|
||||||
|
s.wetter = weather.KaputtFuerTest(t)
|
||||||
|
if err := s.store.SetSettings(store.LocalSub, map[string]string{
|
||||||
|
"weatherCity": "Köln", "weatherLat": "50.938", "weatherLon": "6.96",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
s.routes().ServeHTTP(rec, httptest.NewRequest("GET", "/api/weather", nil))
|
||||||
|
if rec.Code != http.StatusBadGateway {
|
||||||
|
t.Fatalf("bei Ausfall erwartet 502, bekam %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Der Streifen in der Leiste heisst .apps und blendet seine rechten 18 Pixel
|
// Der Streifen in der Leiste heisst .apps und blendet seine rechten 18 Pixel
|
||||||
// mit einer Maske aus. Bekommt ein zweites Element dieselbe Klasse, erbt es die
|
// mit einer Maske aus. Bekommt ein zweites Element dieselbe Klasse, erbt es die
|
||||||
// Maske — die Schublade sah rechts aus, als loese sie sich auf, und in der
|
// Maske — die Schublade sah rechts aus, als loese sie sich auf, und in der
|
||||||
|
|||||||
Reference in new issue
Block a user