Go: store, auth, weather und config sind eigene Pakete unter internal/. Vorher lagen fuenf Dateien in einem Topf, und jede kannte jede. Web: os.js hatte 1425 Zeilen und alles darin. Jetzt core.js (Zustand, Server, Einstellungen), glyphs.js (die Pixelraster), js/widgets/ mit einem Widget je Datei, os.js nur noch die Ansichten. Stylesheet nach css/. Die HoneyBar bleibt eine einzelne Datei ohne Importe — sie wird von fremden Seiten mit einer Zeile eingebunden, da darf nichts nachladen. Nebenbei: Das gebaute Binary lag mit 9,8 MB im Repo und ist jetzt ignoriert. Und der Test gegen eine zweite Versionsnummer geht nicht mehr auf os.js, sondern durch alle .js — sonst haette ihn dieser Umbau still ausgehebelt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
121 lines
4.2 KiB
JavaScript
121 lines
4.2 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
|
|
// 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;
|
|
}
|