/* 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") { 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) => ({ "&": "&", "<": "<", ">": ">" })[c]); } export function escapeAttr(s) { return escapeHtml(s).replace(/"/g, """); } /* 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; }