Files
HoneyOS/web/js/widgets/bild.js
T
marc c0abfbb491 Zwei Widgets: Zettel mit kleinem Markdown, Bild je Nutzer
Zettel: ein Textfeld, mehr nicht — InkNote gibt es schon. Gelesen wird
gesetzt (Ueberschriften, Haken, Aufzaehlungen, fett/kursiv/code, Strich),
geschrieben wird roh. Haken lassen sich ohne Bearbeiten setzen und
aendern die Quelle. Gespeichert wird 800 ms nach dem letzten Anschlag,
sofort beim Verlassen und beim Schliessen des Tabs (keepalive statt
sendBeacon: der kann nur POST und traegt keine Kopfzeilen). Grenze 4000
Zeichen, vom Server durchgesetzt.

Bild: genau eines je Nutzer, hochgeladen oder verlinkt. Die erste Datei,
die HoneyOS ablegt — in bilder/ neben honeyos.json, benannt nach einem
Fingerabdruck der Nutzerkennung. Ein neues Bild ersetzt das alte samt
Datei, die Adresse traegt einen Versionsanteil gegen den Zwischen-
speicher. SVG kommt nicht herein (Text, kann Skripte tragen), und der
Typ wird an den Bytes erkannt, nicht an der Angabe des Browsers.

Der Rahmen kann jetzt Widgets mit eigenem DOM: 'stabil' haelt den Inhalt,
'montiert' verdrahtet ihn. Ohne das haette das Anklopfen bei den Apps
alle zwei Minuten den Text unter den Fingern ausgetauscht.

Nebenbei: Der Aufrufhelfer setzte bei jedem Schreibzugriff
Content-Type: application/json — auch fuer Bildbytes.
2026-08-31 00:44:20 +02:00

135 lines
4.4 KiB
JavaScript

import { api, escapeAttr, escapeHtml } from "../core.js";
import { widgetAnmelden, widgetsLaden, widgetsZeichnen } from "./rahmen.js";
/* ── Das Bild ──────────────────────────────────────────────────────────────
*
* Genau **ein** Bild je Nutzer, hochgeladen oder verlinkt. Ein neues ersetzt
* das alte, ohne Verwaltung und ohne Liste — das ist der ganze Reiz: Es ist
* das erste Widget, das nichts abfragt, sondern etwas *ist*.
*
* 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.
*/
const LEER = "Kein Bild — hochladen oder Adresse einfügen.";
let art = ""; // "datei" | "link" | ""
let quelle = "";
let stand = ""; // Meldung unter dem Bild
let linkAuf = false;
widgetAnmelden({
id: "bild",
titel: "bild",
// Hält ein Dateifeld und ein Eingabefeld: Der Rahmen darf den Inhalt nicht
// unter den Fingern austauschen.
stabil: true,
async laden() {
const d = await api("/api/image/info");
art = (d && d.art) || "";
quelle = (d && d.url) || "";
},
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="zettel-fuss">
<span class="zettel-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>`;
},
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");
});
}
},
});
async function hochladen(file) {
melden("lädt hoch …");
try {
await api("/api/image", { method: "POST", body: file, headers: { "Content-Type": file.type || "application/octet-stream" } });
linkAuf = false;
await widgetsLaden("bild");
melden("");
} catch (e) {
melden(e.message);
}
}
async function linkSetzen(adresse) {
melden(adresse ? "übernehme …" : "entferne …");
try {
await api("/api/image", { method: "PUT", body: JSON.stringify({ url: adresse }) });
linkAuf = false;
await widgetsLaden("bild");
melden("");
} catch (e) {
melden(e.message);
}
}
function melden(s) {
stand = s;
document.querySelectorAll('[data-widget="bild"] .zettel-stand').forEach((el) => {
el.textContent = s;
});
}
/** Neu zeichnen — stabile Widgets lässt der Rahmen sonst in Ruhe. */
function neu() {
document.querySelectorAll('[data-widget="bild"]').forEach((el) => delete el.dataset.zustand);
widgetsZeichnen();
}