Files
HoneyOS/web/js/core.js
T
marcandClaude Opus 5 501c3b8930 Apps lassen sich in Kategorien gruppieren
Ein Feld an der App, kein zweites Datenmodell: Eine Kategorie besteht genau
so lange, wie eine App sie traegt. Damit gibt es nichts anzulegen, nichts zu
loeschen und keine zweite Reihenfolge, die mit der bestehenden aus dem Tritt
geraten koennte.

Die Reihenfolge der Ueberschriften ist die ihres ersten Auftretens in der
Liste -- also ordnen dieselben Pfeile wie bisher beides. Am Rand einer Gruppe
haengt der Pfeil die App in die Nachbargruppe um, statt zu tauschen: derselbe
Knopf zum Sortieren und zum Umkategorisieren, und keine App kann zwischen
zwei Gruppen liegen bleiben.

Eingesammelt wird im Server, beim Schreiben wie beim Lesen. Das war nicht der
erste Entwurf: Zuerst sortierte die Oberflaeche und die HoneyBar setzte ihre
Ueberschriften beim Durchlaufen -- prompt stand eine App ohne Kategorie in der
HoneyBar unter der letzten Ueberschrift und auf dem Schreibtisch ganz oben.
Bei zwei Lesern derselben Liste gehoert die Reihenfolge dorthin, wo die Liste
liegt.

Ueberschriften erscheinen, wo man sucht: Kacheln, Schublade beider Leisten,
Programme-Fenster. Nicht im App-Streifen (32 Pixel) und nicht bei den
Schreibtischsymbolen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:12:03 +02:00

156 lines
5.9 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) => ({ "&": "&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;
}
/* ── Kategorien ────────────────────────────────────────────────────────────
*
* Apps tragen eine Kategorie als freien Text. Eine Liste von Kategorien gibt
* es nicht: Eine Kategorie besteht, solange eine App sie trägt. Deshalb ist
* dies auch keine Sortierung, sondern nur ein Einsammeln — die Reihenfolge
* der Gruppen ist die ihres ersten Auftretens, die Reihenfolge innerhalb
* einer Gruppe die der App-Liste. Damit ordnen dieselben Pfeile wie bisher
* beides zugleich, und es gibt nichts, was auseinanderlaufen könnte.
*
* Apps ohne Kategorie bilden die erste Gruppe, und die trägt keine
* Überschrift: Wer nie eine Kategorie vergibt, sieht alles wie zuvor.
*/
export function gruppiere(apps) {
const gruppen = [];
const nach = new Map();
for (const app of apps) {
const k = (app.kategorie || "").trim();
if (!nach.has(k)) {
const g = { kategorie: k, apps: [] };
nach.set(k, g);
gruppen.push(g);
}
nach.get(k).apps.push(app);
}
// Ohne Kategorie zuerst — das ist der Stand von vorher, und der gehört nach
// oben, nicht dahin, wo zufällig die erste unbenannte App steht.
gruppen.sort((a, b) => (a.kategorie === "" ? -1 : b.kategorie === "" ? 1 : 0));
return gruppen;
}