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.
This commit is contained in:
marc committed 2026-08-31 01:24:50 +02:00
1 parent c0abfbb491
commit 3807ddca24
12 files changed
+806 -220

No files matched your search

+3 -3
View File
@@ -48,8 +48,8 @@ widgetAnmelden({
</div>`
: ""
}
<div class="zettel-fuss">
<span class="zettel-stand">${escapeHtml(stand)}</span>
<div class="w-fuss">
<span class="w-stand">${escapeHtml(stand)}</span>
<span class="bildw-knoepfe">
<input class="bildw-datei" type="file" accept="image/png,image/jpeg,image/webp,image/gif" hidden />
<button class="btn bildw-waehlen">${quelle ? "ersetzen" : "hochladen"}</button>
@@ -122,7 +122,7 @@ async function linkSetzen(adresse) {
function melden(s) {
stand = s;
document.querySelectorAll('[data-widget="bild"] .zettel-stand').forEach((el) => {
document.querySelectorAll('[data-widget="bild"] .w-stand').forEach((el) => {
el.textContent = s;
});
}
+357 -69
View File
@@ -1,33 +1,57 @@
import { api, escapeHtml } from "../core.js";
import { widgetAnmelden, widgetsZeichnen } from "./rahmen.js";
/* ── Der Zettel ────────────────────────────────────────────────────────────
/* ── Die Notiz ─────────────────────────────────────────────────────────────
*
* Ein Textfeld, mehr nicht. **InkNote gibt es schon** — der Zettel darf keine
* zweite Notiz-App werden: kein Titel, keine Liste, keine Ordner, keine Suche.
* Genau ein Text, den man im Vorbeigehen benutzt: Einkaufszettel, eine Nummer,
* ein Gedanke. Alles, was länger als einen Tag lebt, gehört nach InkNote.
* Ein Text, mehr nicht. **InkNote gibt es schon** — die Notiz darf keine zweite
* Notiz-App werden: kein Titel, keine Liste von Notizen, keine Ordner, keine
* Suche. Genau ein Text, den man im Vorbeigehen benutzt: Einkaufszettel, eine
* Nummer, ein Gedanke. Alles, was länger als einen Tag lebt, gehört nach
* InkNote.
*
* Zwei Ansichten: gelesen wird gesetzt, geschrieben wird roh. Der Umweg über
* einen Knopf ist Absicht — ein Feld, das immer im Bearbeitungsmodus steht,
* frisst jeden Klick, den man eigentlich dem Häkchen zugedacht hatte.
* SOFORT-ANSICHT — warum zeilenweise?
*
* Häkchen lassen sich **ohne** Bearbeiten setzen. Das ist der häufigste Griff
* an einer Liste, und dafür erst in den Text zu springen wäre albern.
* Vorher gab es zwei Ansichten: gelesen gesetzt, geschrieben roh. Wer schrieb,
* sah `- [ ] Milch` und musste sich merken, wie das gleich aussehen wird. Das
* ist die Verwirrung, die hier weg soll.
*
* Der Text ist deshalb **immer** gesetzt zu sehen; roh ist nur die eine Zeile,
* in der die Schreibmarke steht. Man tippt also in einer Aufzählung, sieht aber
* ringsum schon Überschriften, Häkchen und Punkte.
*
* Der Weg dorthin wäre auch anders gegangen — ein `contenteditable`, das
* fertiges HTML entgegennimmt und daraus wieder Markdown macht. Das ist die
* Bauform, die in jedem Editor Ärger macht: Der Browser erzeugt beim Tippen
* eigenes HTML (`<div>`, `<span style>`, beim Einfügen ganze Fremdkörper), und
* aus dem muss man den Quelltext *zurückraten*. Ein Fehler dabei kostet Text.
*
* Hier bleibt der Quelltext die Wahrheit und wird nie zurückgerechnet: Die
* Anzeige entsteht aus ihm, und geändert wird immer nur genau eine Zeile.
* Deshalb kann hier nichts verlorengehen, was man nicht selbst gelöscht hat.
*
* Für größere Umbauten (alles markieren, Absätze verschieben) gibt es den
* Knopf „quelltext": dann doch das ganze Feld, roh.
*/
const LEER = "Noch nichts notiert.";
const VERZUG = 800; // ms bis zum Speichern beim Tippen
let text = "";
let bearbeiten = false;
let zeile = -1; // welche Zeile gerade roh bearbeitet wird, -1 = keine
let marke = 0; // wohin die Schreibmarke nach dem Neuzeichnen gehört
let roh = false; // ganzes Feld roh (der Umweg für größere Umbauten)
let stand = ""; // "speichert …" | "gespeichert" | Fehlertext
let uhr = null;
const zeilen = () => text.split("\n");
const setzen = (arr) => {
text = arr.join("\n");
};
widgetAnmelden({
id: "notiz",
titel: "zettel",
// Der Zettel hält ein Textfeld: Sein Inhalt darf nicht bei jedem Neuzeichnen
titel: "notiz",
// Die Notiz hält ein Textfeld: Ihr Inhalt darf nicht bei jedem Neuzeichnen
// ersetzt werden, sonst verliert man beim Tippen Text und Schreibmarke.
stabil: true,
@@ -37,54 +61,72 @@ widgetAnmelden({
},
inhalt() {
if (bearbeiten) {
if (roh) {
return `
<textarea class="zettel-feld" spellcheck="false"
${werkzeuge()}
<textarea class="notiz-feld" spellcheck="false"
placeholder="# Einkauf&#10;- [ ] Milch&#10;- [x] Brot">${escapeHtml(text)}</textarea>
<div class="zettel-fuss">
<span class="zettel-stand">${escapeHtml(stand)}</span>
<button class="btn zettel-fertig">fertig</button>
</div>`;
${fuss("fertig")}`;
}
return `
<div class="zettel-text">${text.trim() ? markdown(text) : `<p class="w-zustand">${LEER}</p>`}</div>
<div class="zettel-fuss">
<span class="zettel-stand">${escapeHtml(stand)}</span>
<button class="btn zettel-auf">${text.trim() ? "bearbeiten" : "schreiben"}</button>
</div>`;
const koerper = text.trim() || zeile >= 0
? `<div class="notiz-text">${markdown(text, zeile)}</div>`
: `<div class="notiz-text notiz-leer"><p class="w-zustand">${LEER}</p></div>`;
return `${werkzeuge()}${koerper}${fuss("quelltext")}`;
},
montiert(el) {
const feld = el.querySelector(".zettel-feld");
if (feld) {
el.querySelectorAll("[data-tat]").forEach((b) => {
// Nicht auf „click": Ein Mausklick nimmt dem Textfeld vorher den Fokus,
// und mit ihm wüsste man nicht mehr, für welche Zeile der Knopf gilt.
b.addEventListener("mousedown", (e) => {
e.preventDefault();
tat(b.dataset.tat);
});
});
if (roh) {
const feld = el.querySelector(".notiz-feld");
feld.focus();
// Schreibmarke ans Ende, nicht an den Anfang: Man schreibt weiter, man
// fängt selten neu an.
feld.setSelectionRange(feld.value.length, feld.value.length);
feld.addEventListener("input", () => {
text = feld.value;
spaeterSpeichern();
});
// Beim Verlassen sofort, nicht erst nach dem Verzug — sonst ginge der
// letzte Satz verloren, wenn man den Tab zumacht.
feld.addEventListener("blur", jetztSpeichern);
el.querySelector(".zettel-fertig").addEventListener("click", () => {
bearbeiten = false;
jetztSpeichern();
neu();
});
return;
}
const auf = el.querySelector(".zettel-auf");
if (auf) {
auf.addEventListener("click", () => {
bearbeiten = true;
neu();
const eingabe = el.querySelector(".notiz-zeile");
if (eingabe) {
eingabe.focus();
eingabe.setSelectionRange(marke, marke);
wachsen(eingabe);
eingabe.addEventListener("input", () => {
const z = zeilen();
z[zeile] = eingabe.value;
setzen(z);
wachsen(eingabe);
spaeterSpeichern();
});
eingabe.addEventListener("keydown", (e) => taste(e, eingabe));
}
// Häkchen im gesetzten Text — sie ändern die Quelle, nicht nur die Anzeige.
el.querySelectorAll("[data-hak]").forEach((box) =>
// Anklicken heißt: hier weiterschreiben.
el.querySelectorAll("[data-z]").forEach((b) =>
b.addEventListener("mousedown", (e) => {
if (e.target.closest(".notiz-zeile, .notiz-hakbox")) return;
e.preventDefault();
const i = Number(b.dataset.z);
springen(i, zeilen()[i].length);
})
);
el.querySelector(".notiz-leer")?.addEventListener("mousedown", (e) => {
e.preventDefault();
springen(0, 0);
});
// Häkchen lassen sich setzen, ohne in den Text zu springen — das ist der
// häufigste Griff an einer Liste.
el.querySelectorAll(".notiz-hakbox").forEach((box) =>
box.addEventListener("change", () => {
text = hakenUmlegen(text, Number(box.dataset.hak));
neu();
@@ -94,13 +136,229 @@ widgetAnmelden({
},
});
/* ── Die kleine Werkzeugleiste ─────────────────────────────────────────────
* Sechs Knöpfe, keine Auswahlfelder. Alles, was das kleine Markdown kann, und
* nichts darüber hinaus — eine Leiste mit Knöpfen für Dinge, die der Zettel
* gar nicht kennt, wäre schlimmer als keine.
*/
const KNOEPFE = [
["haken", "☐", "Aufgabe"],
["punkt", "•", "Aufzählung"],
["nummer", "1.", "Nummerierte Liste"],
["ueber", "H", "Überschrift"],
["fett", "B", "Fett"],
["linie", "—", "Trennlinie"],
];
function werkzeuge() {
return (
`<div class="notiz-werkzeuge">` +
KNOEPFE.map(
([tat, zeichen, titel]) =>
`<button type="button" class="notiz-wz nw-${tat}" data-tat="${tat}" title="${titel}" ${
roh ? "disabled" : ""
}>${zeichen}</button>`
).join("") +
`</div>`
);
}
function fuss(knopf) {
return `
<div class="notiz-fuss">
<span class="notiz-stand">${escapeHtml(stand)}</span>
<button type="button" class="btn notiz-modus" data-tat="modus">${knopf}</button>
</div>`;
}
/** Was die Knöpfe tun. Alle arbeiten auf der Zeile, in der man gerade steht. */
function tat(name) {
if (name === "modus") {
roh = !roh;
zeile = -1;
jetztSpeichern();
neu();
return;
}
// Ohne Schreibmarke gibt es keine „aktuelle Zeile" — dann eine neue am Ende.
if (zeile < 0) {
const z = zeilen();
if (z[z.length - 1].trim()) z.push("");
setzen(z);
zeile = z.length - 1;
marke = 0;
}
const z = zeilen();
if (name === "linie") {
z.splice(zeile + (z[zeile].trim() ? 1 : 0), 0, "---", "");
setzen(z);
zeile += z[zeile].trim() ? 2 : 1;
marke = 0;
neu();
jetztSpeichern();
return;
}
if (name === "fett") {
const feld = document.querySelector(".notiz-zeile");
const a = feld ? feld.selectionStart : z[zeile].length;
const b = feld ? feld.selectionEnd : a;
const wort = z[zeile].slice(a, b);
z[zeile] = z[zeile].slice(0, a) + "**" + wort + "**" + z[zeile].slice(b);
marke = wort ? b + 4 : a + 2; // ohne Auswahl: zwischen die Sterne
setzen(z);
neu();
jetztSpeichern();
return;
}
const VORSATZ = { haken: "- [ ] ", punkt: "- ", nummer: "1. ", ueber: "# " };
const alt = vorsatz(z[zeile]);
const rest = z[zeile].slice(alt.length);
const neuerVorsatz = alt === VORSATZ[name] ? "" : VORSATZ[name];
z[zeile] = neuerVorsatz + rest;
marke = Math.max(0, marke - alt.length) + neuerVorsatz.length;
setzen(z);
neu();
jetztSpeichern();
}
/** Der Listen- oder Überschriften-Vorsatz einer Zeile, samt Leerzeichen. */
function vorsatz(z) {
const m = /^(\s*(?:[-*]\s+\[[ xX]\]\s+|[-*]\s+|\d+[.)]\s+|#{1,3}\s+))/.exec(z);
return m ? m[1] : "";
}
/* ── Tastatur in der einen rohen Zeile ─────────────────────────────────── */
function taste(e, feld) {
const i = zeile;
const z = zeilen();
const anfang = feld.selectionStart === 0 && feld.selectionEnd === 0;
const ende = feld.selectionStart === feld.value.length && feld.selectionEnd === feld.value.length;
if (e.key === "Enter") {
e.preventDefault();
const links = feld.value.slice(0, feld.selectionStart);
const rechts = feld.value.slice(feld.selectionEnd);
const v = vorsatz(links);
// Enter auf einer leeren Aufzählungszeile beendet die Liste, statt einen
// weiteren leeren Punkt anzuhängen.
if (v && !links.slice(v.length).trim() && !rechts) {
z[i] = "";
setzen(z);
springen(i, 0);
return;
}
z.splice(i, 1, links, weiterfuehren(v) + rechts);
setzen(z);
springen(i + 1, weiterfuehren(v).length);
return;
}
if (e.key === "Backspace" && anfang) {
const v = vorsatz(feld.value);
if (v) {
// Erst den Vorsatz weg, dann erst die Zeile: „Backspace am Zeilenanfang"
// heißt meistens „das ist doch kein Aufzählungspunkt".
e.preventDefault();
z[i] = feld.value.slice(v.length);
setzen(z);
springen(i, 0);
return;
}
if (i > 0) {
e.preventDefault();
const stelle = z[i - 1].length;
z.splice(i - 1, 2, z[i - 1] + z[i]);
setzen(z);
springen(i - 1, stelle);
}
return;
}
if (e.key === "Delete" && ende && i < z.length - 1) {
e.preventDefault();
const stelle = z[i].length;
z.splice(i, 2, z[i] + z[i + 1]);
setzen(z);
springen(i, stelle);
return;
}
if (e.key === "ArrowUp" && anfang && i > 0) {
e.preventDefault();
springen(i - 1, z[i - 1].length);
return;
}
if (e.key === "ArrowDown" && ende && i < z.length - 1) {
e.preventDefault();
springen(i + 1, 0);
return;
}
if (e.key === "Escape") {
e.preventDefault();
beenden();
}
}
/** Was nach Enter am Anfang der neuen Zeile steht. */
function weiterfuehren(v) {
if (!v || v.trim().startsWith("#")) return "";
const haken = /^(\s*[-*]\s+)\[[ xX]\]\s+$/.exec(v);
if (haken) return haken[1] + "[ ] "; // eine neue Aufgabe ist offen
const nummer = /^(\s*)(\d+)([.)]\s+)$/.exec(v);
if (nummer) return nummer[1] + (Number(nummer[2]) + 1) + nummer[3];
return v;
}
function springen(i, stelle) {
zeile = i;
marke = stelle;
neu();
}
function beenden() {
zeile = -1;
neu();
jetztSpeichern();
}
// Ein Klick irgendwo anders hin heißt: fertig mit dieser Zeile. Nicht über
// „blur" — den löst auch jeder Knopf in der Werkzeugleiste aus.
//
// In der **Erfassungsphase**, und das ist hier keine Feinheit: Ein Klick in die
// Notiz zeichnet sie neu, und danach hängt das angeklickte Element nicht mehr
// im Dokument. `closest()` liefe dann ins Leere und dieser Zuhörer hielte einen
// Klick auf die Notiz für einen Klick nach draußen — er würde also genau das
// beenden, was der Klick gerade geöffnet hat.
document.addEventListener(
"mousedown",
(e) => {
if (zeile < 0 || e.target.closest('[data-widget="notiz"]')) return;
beenden();
},
true
);
/** Neu zeichnen — der Rahmen lässt stabile Widgets sonst in Ruhe. */
function neu() {
const koerper = document.querySelectorAll('[data-widget="notiz"]');
koerper.forEach((el) => delete el.dataset.zustand);
document.querySelectorAll('[data-widget="notiz"]').forEach((el) => delete el.dataset.zustand);
widgetsZeichnen();
}
/** Ein Zeilenfeld ist so hoch wie sein Text — es soll aussehen wie die Zeile,
* die es ersetzt, nicht wie ein Formular. */
function wachsen(feld) {
feld.style.height = "auto";
feld.style.height = feld.scrollHeight + "px";
}
/* ── Speichern ─────────────────────────────────────────────────────────── */
function spaeterSpeichern() {
stand = "";
clearTimeout(uhr);
@@ -110,8 +368,8 @@ function spaeterSpeichern() {
/**
* Speichert, wenn sich etwas geändert hat.
*
* Zwei offene Geräte überschreiben einander; der letzte Stand gewinnt. Für
* einen Zettel ist das vertretbar — man sollte es nur wissen und nicht später
* Zwei offene Geräte überschreiben einander; der letzte Stand gewinnt. Für eine
* Notiz ist das vertretbar — man sollte es nur wissen und nicht später
* „Datenverlust" nennen.
*/
let gespeichert = null;
@@ -131,7 +389,7 @@ async function jetztSpeichern() {
function standSetzen(s) {
stand = s;
document.querySelectorAll('[data-widget="notiz"] .zettel-stand').forEach((el) => {
document.querySelectorAll('[data-widget="notiz"] .notiz-stand').forEach((el) => {
el.textContent = s;
});
}
@@ -142,15 +400,27 @@ function standSetzen(s) {
* Aufzählungen, fett, kursiv, Code. Keine Bibliothek — HoneyOS hat keinen
* Bauschritt, und für sechs Regeln lohnt kein Nachladen.
*
* **Zuerst wird escapt, dann gesetzt.** Der Text ist zwar der eigene, aber ein
* Zettel, der `<script>` ausführt, wäre eine Falle für den Tag, an dem man
* etwas hineinkopiert.
* **Zuerst wird escapt, dann gesetzt.** Der Text ist zwar der eigene, aber eine
* Notiz, die `<script>` ausführt, wäre eine Falle für den Tag, an dem man etwas
* hineinkopiert.
*
* `bearbeitet` ist die Zeile, die roh gezeigt wird. Sie behält ihren Platz in
* der Liste, in der sie steht — sonst spränge der Punkt beim Hineinklicken aus
* der Aufzählung heraus.
*/
export function markdown(quelle) {
const zeilen = quelle.split("\n");
export function markdown(quelle, bearbeitet = -1) {
const zln = quelle.split("\n");
let html = "";
let liste = null; // "ul" | "ol" | null
let hakenNr = -1; // fortlaufend über den ganzen Text, für data-hak
// Die Häkchen werden über den **Quelltext** gezählt, nicht über das, was man
// sieht: Die gerade bearbeitete Zeile zeigt keines, ihre Nummer muss aber
// vergeben bleiben, sonst schaltet ein Klick darunter das falsche um.
const hakNr = {};
let n = -1;
zln.forEach((z, i) => {
if (HAKEN.test(z)) hakNr[i] = ++n;
});
const listeZu = () => {
if (liste) html += `</${liste}>`;
@@ -164,8 +434,23 @@ export function markdown(quelle) {
}
};
zeilen.forEach((roh) => {
const z = roh.trimEnd();
zln.forEach((rohzeile, i) => {
const z = rohzeile.trimEnd();
if (i === bearbeitet) {
const feld =
`<textarea class="notiz-zeile" rows="1" spellcheck="false" ` +
`placeholder="schreiben …">${escapeHtml(rohzeile)}</textarea>`;
const art = HAKEN.test(z) || PUNKT.test(z) ? "ul" : ZAHL.test(z) ? "ol" : null;
if (art) {
listeAuf(art);
html += `<li class="notiz-rohli">${feld}</li>`;
} else {
listeZu();
html += feld;
}
return;
}
if (!z.trim()) {
listeZu();
@@ -175,51 +460,54 @@ export function markdown(quelle) {
const ueber = /^(#{1,3})\s+(.*)$/.exec(z);
if (ueber) {
listeZu();
const n = ueber[1].length + 2; // # → h3, ## → h4, ### → h5
html += `<h${n}>${inline(ueber[2])}</h${n}>`;
const g = ueber[1].length + 2; // # → h3, ## → h4, ### → h5
html += `<h${g} data-z="${i}">${inline(ueber[2])}</h${g}>`;
return;
}
if (/^(-{3,}|\*{3,})$/.test(z.trim())) {
listeZu();
html += "<hr />";
html += `<hr data-z="${i}" />`;
return;
}
const hak = /^\s*[-*]\s+\[([ xX])\]\s+(.*)$/.exec(z);
const hak = HAKEN.exec(z);
if (hak) {
hakenNr++;
listeAuf("ul");
const an = hak[1].toLowerCase() === "x";
html +=
`<li class="zettel-hak${an ? " ab" : ""}">` +
`<label><input type="checkbox" data-hak="${hakenNr}"${an ? " checked" : ""} />` +
`<li class="notiz-hak${an ? " ab" : ""}" data-z="${i}">` +
`<label><input class="notiz-hakbox" type="checkbox" data-hak="${hakNr[i]}"${an ? " checked" : ""} />` +
`<span>${inline(hak[2])}</span></label></li>`;
return;
}
const punkt = /^\s*[-*]\s+(.*)$/.exec(z);
const punkt = PUNKT.exec(z);
if (punkt) {
listeAuf("ul");
html += `<li>${inline(punkt[1])}</li>`;
html += `<li data-z="${i}">${inline(punkt[1])}</li>`;
return;
}
const zahl = /^\s*\d+[.)]\s+(.*)$/.exec(z);
const zahl = ZAHL.exec(z);
if (zahl) {
listeAuf("ol");
html += `<li>${inline(zahl[1])}</li>`;
html += `<li data-z="${i}">${inline(zahl[1])}</li>`;
return;
}
listeZu();
html += `<p>${inline(z)}</p>`;
html += `<p data-z="${i}">${inline(z)}</p>`;
});
listeZu();
return html;
}
const HAKEN = /^\s*[-*]\s+\[([ xX])\]\s+(.*)$/;
const PUNKT = /^\s*[-*]\s+(.*)$/;
const ZAHL = /^\s*\d+[.)]\s+(.*)$/;
function inline(s) {
return escapeHtml(s)
.replace(/`([^`]+)`/g, "<code>$1</code>")
+74 -13
View File
@@ -28,12 +28,15 @@
* als Aufklappfeld in der HoneyBar leben können — deshalb erzeugt `inhalt()`
* nur den Kern, und die Verpackung entsteht hier.
*/
import { $, state } from "../core.js";
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);
@@ -55,9 +58,9 @@ export function widgetListe() {
* 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.
* 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) => {
@@ -67,6 +70,53 @@ export function widgetsOrdnen(ids) {
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.
*
@@ -75,9 +125,15 @@ export function widgetsOrdnen(ids) {
* anderen Widgets mitreißen.
*/
export async function widgetsLaden(nurId) {
const dran = REGISTER.filter((w) => w.laden && (!nurId || w.id === 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 {
@@ -94,7 +150,9 @@ export async function widgetsLaden(nurId) {
export function widgetsStarten() {
REGISTER.forEach((w) => {
if (!w.takt || uhren[w.id]) return;
uhren[w.id] = setInterval(() => widgetsLaden(w.id), w.takt);
uhren[w.id] = setInterval(() => {
if (widgetPlan().find((p) => p.id === w.id)?.an) widgetsLaden(w.id);
}, w.takt);
});
}
@@ -175,13 +233,16 @@ export function widgetsZeichnen() {
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");
});
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);
-2
View File
@@ -11,8 +11,6 @@ import { widgetAnmelden, widgetsLaden } from "./rahmen.js";
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