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.
126 lines
4.6 KiB
JavaScript
126 lines
4.6 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 in der HoneyBar. Auf dem Schreibtisch entscheidet das die Widget-
|
|
// Liste — zwei Schalter für dasselbe Widget wären einer zu viel.
|
|
weatherBar: "off",
|
|
weatherCity: "",
|
|
weatherLat: "",
|
|
weatherLon: "",
|
|
// Reihenfolge und An/Aus der Widgets, als eine Liste: "benutzer,-wetter,notiz".
|
|
// Leer heißt: Grundstellung (siehe widgets/rahmen.js).
|
|
widgets: "",
|
|
};
|
|
|
|
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) => ({ "&": "&", "<": "<", ">": ">" })[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;
|
|
}
|