Files
HoneyOS/web/js/core.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

125 lines
4.5 KiB
JavaScript

/* Der gemeinsame Unterbau: Zustand, Server, Einstellungen, Kleinkram.
*
* Alles, was mehr als eine Datei braucht, steht hier — und nur das. Die
* Ansichten (os.js) und die Widgets holen es sich von hier, statt sich
* gegenseitig zu kennen.
*/
export const LS_SETTINGS = "honeyos.settings";
export const SS_BOOTED = "honeyos.booted";
export const DEFAULT_SETTINGS = {
mode: "terminal", // terminal | desktop
phosphor: "amber", // amber | green | ice
scanlines: "on",
flicker: "on",
glow: "on",
openIn: "tab", // tab | same
// Symbole im App-Streifen beider Leisten. Im Aufklappmenü stehen sie immer.
barGlyphs: "on",
// Wetter. Zwei Schalter statt einem: Der Schreibtisch und die HoneyBar sind
// zwei verschiedene Orte, und wer das Wetter beim Arbeiten in einer App will,
// will es nicht zwingend auch auf einer Seite, die er selten aufruft.
weatherBar: "off",
weatherDesk: "off",
weatherCity: "",
weatherLat: "",
weatherLon: "",
};
export const state = {
cfg: { user: "", hostname: "honeyos", version: "…", apps: [] },
me: null, // {sub, name, picture} sobald angemeldet
weather: null, // zuletzt geholtes Wetter, oder null
oidc: false,
settings: { ...DEFAULT_SETTINGS },
probes: {}, // appId -> "up" | "down" | "unknown"
wins: {}, // id -> {el, task, min}
zTop: 10,
};
export const $ = (id) => document.getElementById(id);
export const isTouch = () => window.matchMedia("(pointer: coarse)").matches;
/* ── Server ────────────────────────────────────────────────────────────────
* Ein winziger Aufrufhelfer. Schreibende Aufrufe brauchen die CSRF-Kopfzeile:
* ihr Wert steht in einem absichtlich lesbaren Cookie, das nur bekommt, wer
* die Seite wirklich geöffnet hat.
*/
/** Der Schlüssel für die App-Liste steht als Meta-Angabe in der Seite — der
* Server legt ihn dort hinein, wenn einer eingerichtet ist. */
export const APPS_KEY = document.querySelector('meta[name="honeyos-key"]')?.content || "";
export async function api(path, opts = {}) {
const headers = { ...(opts.headers || {}) };
if (APPS_KEY) headers["X-HoneyOS-Key"] = APPS_KEY;
if (opts.method && opts.method !== "GET") {
// Nur voreinstellen, nicht überschreiben: Das Bild-Widget schickt Bytes und
// nennt deren Typ selbst. Vorher log der Aufrufhelfer den Server an.
if (!headers["Content-Type"]) headers["Content-Type"] = "application/json";
const tok = cookie("honeyos_csrf");
if (tok) headers["X-CSRF-Token"] = tok;
}
const res = await fetch(path, { credentials: "same-origin", cache: "no-store", ...opts, headers });
if (res.status === 204) return null;
const body = await res.json().catch(() => null);
if (!res.ok) throw new Error((body && body.error) || `${res.status} ${res.statusText}`);
return body;
}
export function cookie(name) {
return document.cookie
.split(";")
.map((c) => c.trim().split("="))
.find(([k]) => k === name)?.[1] || "";
}
export function loadLocalSettings() {
state.settings = { ...DEFAULT_SETTINGS, ...(readJson(LS_SETTINGS) || {}) };
}
export function readJson(key) {
try {
return JSON.parse(localStorage.getItem(key) || "null");
} catch {
return null;
}
}
let syncTimer = null;
export function saveSettings() {
try {
localStorage.setItem(LS_SETTINGS, JSON.stringify(state.settings));
} catch {
/* Privatmodus: dann gilt es nur für diese Sitzung — der Server merkt es trotzdem */
}
applySettings();
// Zum Server gebündelt: wer dreimal hintereinander die Röhre wechselt,
// soll nicht dreimal schreiben lassen.
clearTimeout(syncTimer);
syncTimer = setTimeout(() => {
api("/api/settings", { method: "PUT", body: JSON.stringify(state.settings) }).catch((e) =>
console.warn("Einstellungen nicht gespeichert:", e.message)
);
}, 600);
}
export function escapeHtml(s) {
return String(s).replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
}
export function escapeAttr(s) {
return escapeHtml(s).replace(/"/g, "&quot;");
}
/* Die Einstellungen schlagen als Datenattribute auf die Seite durch; das CSS
hängt daran. Steht hier, weil saveSettings() es sofort anwenden muss. */
export function applySettings() {
const r = document.documentElement;
r.dataset.phosphor = state.settings.phosphor;
r.dataset.scanlines = state.settings.scanlines;
r.dataset.flicker = state.settings.flicker;
r.dataset.glow = state.settings.glow;
r.dataset.mode = state.settings.mode;
}