refactor: aufgeraeumt — Pakete hinten, Widgets vorne
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>
This commit is contained in:
1 parent
d08b0a229c
commit
52deffa30e
20 files changed
+780
-624
No files matched your search
+120
@@ -0,0 +1,120 @@
|
||||
/* 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;
|
||||
}
|
||||
Reference in new issue
Block a user