Bild einstellen gehoert in die Einstellungen, nicht ins Widget

This commit is contained in:
marc committed 2026-08-31 20:12:21 +02:00
1 parent e2988f7ee9
commit 75df4e0833
5 files changed
+151 -65

No files matched your search

+6
View File
@@ -326,6 +326,12 @@ Zu klären, wenn es soweit ist:
- Passt zum Aufnehmen eigener App-Logos (1.3): dieselbe Ablage, dasselbe
Ausliefern. Zusammen bauen spart die Hälfte.
**Nachtrag 31.08.2026.** Hochladen, Verlinken und Entfernen standen erst im
Widget selbst und sind in die Einstellungen gewandert (Marcs Einwand): Das
Widget zeigt, die Einstellungen stellen ein — wie beim Wetter. Damit gilt für
alle Widgets dieselbe Regel, und der Schreibtisch bleibt eine Anzeige statt
einer Bedienoberfläche.
### Notiz-Widget — ✅ gebaut (30.08.2026), Sofort-Ansicht (31.08.2026)
Ein Zettel auf dem Schreibtisch: hineintippen, fertig. Serverseitig je Nutzer,
+4 -1
View File
@@ -279,7 +279,10 @@ gewinnt.
### Das Bild
Genau eines je Nutzer, hochgeladen oder verlinkt; ein neues ersetzt das alte
samt Datei. Erlaubt sind PNG, JPEG, WebP und GIF — der Typ wird aus den
samt Datei. **Eingestellt wird es unter *Einstellungen → Bild*, nicht im Widget**
— wie beim Wetter, dessen Ort auch nicht in der Wetterkachel gesucht wird. Ein
Schild auf dem Schreibtisch, das zu drei Vierteln aus Knöpfen besteht, zeigt vor
allem sich selbst. Erlaubt sind PNG, JPEG, WebP und GIF — der Typ wird aus den
**Bytes** bestimmt, nicht aus dem, was der Browser behauptet. SVG ist bewusst
nicht dabei: Es kann Skripte enthalten. Ausgeliefert wird mit einem
Versionsanteil in der Adresse, sonst bliebe beim Austausch das alte Bild im
+20 -1
View File
@@ -1307,6 +1307,26 @@ body {
.bildw-bild { width: 100%; height: 100%; object-fit: cover; display: block; }
.bildw .w-zustand { padding: 0 12px; text-align: center; }
/* Die Knöpfe stehen in den Einstellungen, nicht im Widget: links eine kleine
Vorschau, rechts was man damit tun kann. */
.bildset { display: flex; gap: 10px; align-items: flex-start; }
.bildset-vorschau {
flex: none;
width: 84px;
height: 56px;
border: 1px solid var(--p-faint);
background: var(--bg);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.bildset-bild { width: 100%; height: 100%; object-fit: cover; display: block; }
.bildset-leer { color: var(--p-dim); font-size: 11px; }
.bildset-rechts { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.bildset-knoepfe { display: flex; gap: 4px; flex-wrap: wrap; }
.bildset-knoepfe .btn { font-size: 12px; padding: 3px 9px; }
.bildw-linkzeile { display: flex; gap: 6px; margin-top: 8px; }
.bildw-linkzeile input {
flex: 1;
@@ -1319,7 +1339,6 @@ body {
padding: 4px 6px;
}
.bildw-linkzeile input:focus { outline: none; border-color: var(--p); }
.bildw-knoepfe { display: flex; gap: 4px; flex-wrap: wrap; justify-content: flex-end; }
/* Einspaltig (Handy, schmales Fenster) sollen die Widgets so breit sein wie das
Systemfenster. Drei verschieden breite Kästen untereinander sehen aus wie ein
+8 -1
View File
@@ -11,7 +11,7 @@ import "./widgets/user.js";
import { statsHtml } from "./widgets/system.js";
import { loadWeather, renderWeatherBar } from "./widgets/weather.js";
import "./widgets/notiz.js";
import "./widgets/bild.js";
import { bildFormularHtml, bildFormularMontieren } from "./widgets/bild.js";
// Die Reihenfolge steht hier und nicht in den Importzeilen darüber: Module
// werden nicht in deren Reihenfolge ausgewertet, sobald eines auf oberster
@@ -590,6 +590,12 @@ function openSettings() {
`<ul class="wl" id="wlListe"></ul>
<p class="sek-hinweis">Reihenfolge und An/Aus liegen bei deinem Konto, nicht am Gerät.</p>`
) +
sektion(
"Bild",
bildFormularHtml() +
`<p class="sek-hinweis">Genau ein Bild je Konto; ein neues ersetzt das alte.
Zu sehen ist es im Widget „bild".</p>`
) +
sektion(
"Wetter",
`<div class="wx-city-row">
@@ -638,6 +644,7 @@ function openSettings() {
});
widgetListeZeichnen(root.querySelector("#wlListe"));
bildFormularMontieren(root);
const zeigeOrt = () => {
root.querySelector("#wxCurrent").textContent = state.settings.weatherCity
+113 -62
View File
@@ -10,92 +10,142 @@ import { widgetAnmelden, widgetsLaden, widgetsZeichnen } from "./rahmen.js";
* Hochgeladen wird zum eigenen Server; verlinkt wird auf eine fremde Adresse.
* Der Unterschied ist keiner der Bequemlichkeit: Ein Link holt bei jedem
* Aufbau des Schreibtischs etwas von woanders, ein Upload nie.
*
* DAS WIDGET ZEIGT, DIE EINSTELLUNGEN STELLEN EIN.
*
* Hochladen, Verlinken und Entfernen standen erst im Widget selbst. Das ist die
* Ausnahme gewesen, nicht die Regel: Der Ort des Wetters wird auch nicht in der
* Wetterkachel gesucht. Ein Schild auf dem Schreibtisch, das zu drei Vierteln
* aus Knöpfen besteht, zeigt vor allem sich selbst.
*
* Also: Das Widget zeigt nur noch das Bild. Die Knöpfe stehen unter
* *Einstellungen → Bild* — dieselbe Datei, aber ein zweiter Ausgang
* (`bildFormularHtml` / `bildFormularMontieren`), den os.js einhängt.
*/
const LEER = "Kein Bild — hochladen oder Adresse einfügen.";
const LEER = "Kein Bild — unter Einstellungen → Bild hochladen oder verlinken.";
const FORM_ID = "bildForm";
let art = ""; // "datei" | "link" | ""
let quelle = "";
let stand = ""; // Meldung unter dem Bild
let stand = ""; // Meldung im Einstellungsfeld
let linkAuf = false;
let kaputt = false; // die Adresse liefert kein Bild
widgetAnmelden({
id: "bild",
titel: "bild",
// Hält ein Dateifeld und ein Eingabefeld: Der Rahmen darf den Inhalt nicht
// unter den Fingern austauschen.
// Das Bild soll beim Neuzeichnen nicht neu geladen werden — ein Bild, das
// alle zwei Minuten kurz weiß aufblitzt, wäre schlechter als keines.
stabil: true,
async laden() {
const d = await api("/api/image/info");
art = (d && d.art) || "";
quelle = (d && d.url) || "";
kaputt = false;
},
inhalt() {
const bild = quelle
? `<img class="bildw-bild" src="${escapeAttr(quelle)}" alt="" />`
: `<p class="w-zustand">${LEER}</p>`;
return `
<div class="bildw">${bild}</div>
${
linkAuf
? `<div class="bildw-linkzeile">
<input class="bildw-url" type="url" spellcheck="false"
placeholder="https://…" value="${escapeAttr(art === "link" ? quelle : "")}" />
<button class="btn bildw-uebernehmen">setzen</button>
</div>`
: ""
}
<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>
<button class="btn bildw-link">link</button>
${quelle ? `<button class="btn bildw-weg">entfernen</button>` : ""}
</span>
</div>`;
if (kaputt) {
return `<div class="bildw"><p class="w-zustand">${
art === "link" ? "Adresse liefert kein Bild" : "Bild nicht lesbar"
}</p></div>`;
}
return `<div class="bildw">${
quelle
? `<img class="bildw-bild" src="${escapeAttr(quelle)}" alt="" />`
: `<p class="w-zustand">${LEER}</p>`
}</div>`;
},
montiert(el) {
const datei = el.querySelector(".bildw-datei");
el.querySelector(".bildw-waehlen").addEventListener("click", () => datei.click());
datei.addEventListener("change", () => datei.files[0] && hochladen(datei.files[0]));
el.querySelector(".bildw-link").addEventListener("click", () => {
linkAuf = !linkAuf;
neu();
});
const url = el.querySelector(".bildw-url");
if (url) {
url.focus();
const setzen = () => linkSetzen(url.value.trim());
el.querySelector(".bildw-uebernehmen").addEventListener("click", setzen);
url.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
setzen();
}
});
}
const weg = el.querySelector(".bildw-weg");
if (weg) weg.addEventListener("click", () => linkSetzen(""));
// Ein Bild, das nicht kommt, soll es sagen — bei einem Link ist das der
// Normalfall des Tages, an dem die fremde Seite umzieht.
const img = el.querySelector(".bildw-bild");
if (img) {
img.addEventListener("error", () => {
melden(art === "link" ? "Adresse liefert kein Bild" : "Bild nicht lesbar");
});
}
el.querySelector(".bildw-bild")?.addEventListener("error", () => {
kaputt = true;
melden(art === "link" ? "Adresse liefert kein Bild" : "Bild nicht lesbar");
neu();
});
},
});
/* ── Der Ausgang in die Einstellungen ──────────────────────────────────── */
/** Das Feld für *Einstellungen → Bild*. os.js hängt es ein, kennt es aber nicht. */
export function bildFormularHtml() {
return `<div id="${FORM_ID}">${formInhalt()}</div>`;
}
/** Nach dem Einhängen verdrahten. */
export function bildFormularMontieren(root) {
const el = root.querySelector(`#${FORM_ID}`);
if (el) verdrahte(el);
}
function formInhalt() {
const vorschau = quelle
? `<img class="bildset-bild" src="${escapeAttr(quelle)}" alt="" />`
: `<span class="bildset-leer">kein bild</span>`;
return `
<div class="bildset">
<span class="bildset-vorschau">${vorschau}</span>
<div class="bildset-rechts">
<div class="bildset-knoepfe">
<input class="bildw-datei" type="file" accept="image/png,image/jpeg,image/webp,image/gif" hidden />
<button type="button" class="btn bildw-waehlen">${quelle ? "ersetzen" : "hochladen"}</button>
<button type="button" class="btn bildw-link">link</button>
${quelle ? `<button type="button" class="btn bildw-weg">entfernen</button>` : ""}
</div>
<span class="w-stand">${escapeHtml(stand)}</span>
</div>
</div>
${
linkAuf
? `<div class="bildw-linkzeile">
<input class="bildw-url" type="url" spellcheck="false"
placeholder="https://…" value="${escapeAttr(art === "link" ? quelle : "")}" />
<button type="button" class="btn bildw-uebernehmen">setzen</button>
</div>`
: ""
}`;
}
function verdrahte(el) {
const datei = el.querySelector(".bildw-datei");
el.querySelector(".bildw-waehlen").addEventListener("click", () => datei.click());
datei.addEventListener("change", () => datei.files[0] && hochladen(datei.files[0]));
el.querySelector(".bildw-link").addEventListener("click", () => {
linkAuf = !linkAuf;
formNeu();
});
const url = el.querySelector(".bildw-url");
if (url) {
url.focus();
const setzen = () => linkSetzen(url.value.trim());
el.querySelector(".bildw-uebernehmen").addEventListener("click", setzen);
url.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
setzen();
}
});
}
el.querySelector(".bildw-weg")?.addEventListener("click", () => linkSetzen(""));
}
/** Das Einstellungsfeld neu zeichnen — es steht nicht unter dem Widget-Rahmen. */
function formNeu() {
const el = document.getElementById(FORM_ID);
if (!el) return;
el.innerHTML = formInhalt();
verdrahte(el);
}
async function hochladen(file) {
melden("lädt hoch …");
try {
@@ -106,6 +156,7 @@ async function hochladen(file) {
} catch (e) {
melden(e.message);
}
formNeu();
}
async function linkSetzen(adresse) {
@@ -118,13 +169,13 @@ async function linkSetzen(adresse) {
} catch (e) {
melden(e.message);
}
formNeu();
}
function melden(s) {
stand = s;
document.querySelectorAll('[data-widget="bild"] .w-stand').forEach((el) => {
el.textContent = s;
});
const el = document.querySelector(`#${FORM_ID} .w-stand`);
if (el) el.textContent = s;
}
/** Neu zeichnen — stabile Widgets lässt der Rahmen sonst in Ruhe. */