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:
marcandClaude Opus 5 committed 2026-08-29 20:51:21 +02:00
1 parent d08b0a229c
commit 52deffa30e
20 files changed
+780 -624

No files matched your search

View File
File renamed without changes.
+2 -2
View File
@@ -6,7 +6,7 @@
<meta name="theme-color" content="#0a0705" />
<title>HoneyOS</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' fill='%230a0705'/%3E%3Cpath d='M4 4h2v3h4V4h2v8h-2V9H6v3H4z' fill='%23ffb641'/%3E%3C/svg%3E" />
<link rel="stylesheet" href="./style.css" />
<link rel="stylesheet" href="./css/style.css" />
</head>
<body>
<!-- Bildschirm: alles liegt darin, damit Scanlines/Vignette darüber passen -->
@@ -110,6 +110,6 @@
<div class="flicker" aria-hidden="true"></div>
</div>
<script src="./os.js"></script>
<script type="module" src="./js/os.js"></script>
</body>
</html>
+120
View File
@@ -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) => ({ "&": "&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;
}
+244
View File
@@ -0,0 +1,244 @@
/* Die Pixelsymbole von HoneyOS.
*
* Eigene Datei, weil sie Daten sind und nicht Verhalten: zwölf mal zwölf
* Zeichen je Symbol, mehr passiert hier nicht. Dieselben Raster stehen noch
* einmal in honeybar/honeybar.js — die soll eine einzelne Datei bleiben, die
* eine fremde App mit einer Zeile einbindet (siehe IDEAS 1.3).
*/
/* ── Pixel-Glyphen ─────────────────────────────────────────────────────────
* 12×12-Raster als Zeichenketten. Ein `#` ist ein Pixel. So bleiben die Icons
* im Retro-Raster statt aus einer Icon-Bibliothek zu kommen.
*/
export const GLYPHS = {
// Wettersymbole. Dieselben Namen wie in weather.go (wmo) und in der HoneyBar —
// solange 1.3 (geteilte Symbolquelle) nicht gebaut ist, stehen sie zweimal da.
sonne: [
"............",
".....##.....",
"..#..##..#..",
"...#....#...",
"....####....",
"..##....##..",
"..##....##..",
"....####....",
"...#....#...",
"..#..##..#..",
".....##.....",
"............",
],
heiter: [
"............",
"...##.......",
"..####..#...",
"...##..###..",
"......#####.",
"...####...#.",
"..#....##.#.",
".##........#",
"#..........#",
".##########.",
"............",
"............",
],
wolke: [
"............",
"....####....",
"...#....#...",
"..#......##.",
".##........#",
"##.........#",
"#..........#",
"#..........#",
".##########.",
"............",
"............",
"............",
],
nebel: [
"............",
"....####....",
"..##....##..",
".##.......#.",
"#..........#",
".##########.",
"............",
".#########..",
"............",
"..#########.",
"............",
".#########..",
],
regen: [
"............",
"....####....",
"...#....#...",
"..#......##.",
".##........#",
"##.........#",
".##########.",
"............",
"..#..#..#...",
".#..#..#....",
"..#..#..#...",
".#..#..#....",
],
schnee: [
"............",
"....####....",
"...#....#...",
"..#......##.",
".##........#",
"##.........#",
".##########.",
"............",
"..#...#...#.",
".#.#.#.#.#..",
"..#...#...#.",
"............",
],
gewitter: [
"............",
"....####....",
"...#....#...",
"..#......##.",
".##........#",
"##.........#",
".##########.",
"......##....",
".....##.....",
"....####....",
"......##....",
".....##.....",
],
cloud: [
"............",
"....####....",
"...#....#...",
"..#......##.",
".##........#",
"##.........#",
"#..........#",
"#..........#",
".##########.",
"............",
"............",
"............",
],
note: [
"..########..",
"..#......#..",
"..#.####.#..",
"..#......#..",
"..#.####.#..",
"..#......#..",
"..#.####.#..",
"..#......#..",
"..#.##...#..",
"..#......#..",
"..########..",
"............",
],
box: [
"............",
".##########.",
".#........#.",
".#........#.",
".#...##...#.",
".#..####..#.",
".#..####..#.",
".#...##...#.",
".#........#.",
".#........#.",
".##########.",
"............",
],
chip: [
"............",
"..#.#.#.#...",
".##########.",
".#........#.",
"##.######.##",
".#.#....#.#.",
"##.#....#.##",
".#.######.#.",
".#........#.",
".##########.",
"..#.#.#.#...",
"............",
],
gear: [
"....####....",
"..#.####.#..",
"..########..",
".####..####.",
"###......###",
"##........##",
"##........##",
"###......###",
".####..####.",
"..########..",
"..#.####.#..",
"....####....",
],
globe: [
"............",
"...######...",
"..#..#...#..",
".#...#....#.",
"############",
"#....#.....#",
"#....#.....#",
"############",
".#...#....#.",
"..#..#...#..",
"...######...",
"............",
],
disk: [
"............",
"############",
"#..######..#",
"#..#....#..#",
"#..#....#..#",
"#..######..#",
"#..........#",
"#.########.#",
"#.#......#.#",
"#.#......#.#",
"############",
"............",
],
folder: [
"............",
".####.......",
"#....#......",
"#.....######",
"#..........#",
"#..........#",
"#..........#",
"#..........#",
"#..........#",
"############",
"............",
"............",
],
};
export function glyphSvg(name) {
const grid = GLYPHS[name] || GLYPHS.box;
const rects = [];
grid.forEach((row, y) => {
// Waagerechte Läufe zusammenfassen: weniger Rechtecke, gleiches Bild.
let run = 0;
for (let x = 0; x <= row.length; x++) {
if (row[x] === "#") {
run++;
} else if (run) {
rects.push(`<rect x="${x - run}" y="${y}" width="${run}" height="1"/>`);
run = 0;
}
}
});
return `<svg viewBox="0 0 12 12" fill="currentColor" shape-rendering="crispEdges" aria-hidden="true">${rects.join("")}</svg>`;
}
+9 -443
View File
@@ -1,3 +1,12 @@
import {
$, api, APPS_KEY, cookie, DEFAULT_SETTINGS, escapeAttr, escapeHtml, isTouch,
applySettings, loadLocalSettings, LS_SETTINGS, readJson, saveSettings,
SS_BOOTED, state,
} from "./core.js";
import { GLYPHS, glyphSvg } from "./glyphs.js";
import { renderUser } from "./widgets/user.js";
import { loadWeather, renderWeather } from "./widgets/weather.js";
/*
* HoneyOS — Desktop-Logik.
*
@@ -16,308 +25,8 @@
* gemeinsamen Teile hier oben und die beiden Ansichten weiter unten getrennt.
*/
const LS_SETTINGS = "honeyos.settings";
const SS_BOOTED = "honeyos.booted";
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: "",
};
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,
};
const $ = (id) => document.getElementById(id);
const isTouch = () => window.matchMedia("(pointer: coarse)").matches;
/* ── Pixel-Glyphen ─────────────────────────────────────────────────────────
* 12×12-Raster als Zeichenketten. Ein `#` ist ein Pixel. So bleiben die Icons
* im Retro-Raster statt aus einer Icon-Bibliothek zu kommen.
*/
const GLYPHS = {
// Wettersymbole. Dieselben Namen wie in weather.go (wmo) und in der HoneyBar —
// solange 1.3 (geteilte Symbolquelle) nicht gebaut ist, stehen sie zweimal da.
sonne: [
"............",
".....##.....",
"..#..##..#..",
"...#....#...",
"....####....",
"..##....##..",
"..##....##..",
"....####....",
"...#....#...",
"..#..##..#..",
".....##.....",
"............",
],
heiter: [
"............",
"...##.......",
"..####..#...",
"...##..###..",
"......#####.",
"...####...#.",
"..#....##.#.",
".##........#",
"#..........#",
".##########.",
"............",
"............",
],
wolke: [
"............",
"....####....",
"...#....#...",
"..#......##.",
".##........#",
"##.........#",
"#..........#",
"#..........#",
".##########.",
"............",
"............",
"............",
],
nebel: [
"............",
"....####....",
"..##....##..",
".##.......#.",
"#..........#",
".##########.",
"............",
".#########..",
"............",
"..#########.",
"............",
".#########..",
],
regen: [
"............",
"....####....",
"...#....#...",
"..#......##.",
".##........#",
"##.........#",
".##########.",
"............",
"..#..#..#...",
".#..#..#....",
"..#..#..#...",
".#..#..#....",
],
schnee: [
"............",
"....####....",
"...#....#...",
"..#......##.",
".##........#",
"##.........#",
".##########.",
"............",
"..#...#...#.",
".#.#.#.#.#..",
"..#...#...#.",
"............",
],
gewitter: [
"............",
"....####....",
"...#....#...",
"..#......##.",
".##........#",
"##.........#",
".##########.",
"......##....",
".....##.....",
"....####....",
"......##....",
".....##.....",
],
cloud: [
"............",
"....####....",
"...#....#...",
"..#......##.",
".##........#",
"##.........#",
"#..........#",
"#..........#",
".##########.",
"............",
"............",
"............",
],
note: [
"..########..",
"..#......#..",
"..#.####.#..",
"..#......#..",
"..#.####.#..",
"..#......#..",
"..#.####.#..",
"..#......#..",
"..#.##...#..",
"..#......#..",
"..########..",
"............",
],
box: [
"............",
".##########.",
".#........#.",
".#........#.",
".#...##...#.",
".#..####..#.",
".#..####..#.",
".#...##...#.",
".#........#.",
".#........#.",
".##########.",
"............",
],
chip: [
"............",
"..#.#.#.#...",
".##########.",
".#........#.",
"##.######.##",
".#.#....#.#.",
"##.#....#.##",
".#.######.#.",
".#........#.",
".##########.",
"..#.#.#.#...",
"............",
],
gear: [
"....####....",
"..#.####.#..",
"..########..",
".####..####.",
"###......###",
"##........##",
"##........##",
"###......###",
".####..####.",
"..########..",
"..#.####.#..",
"....####....",
],
globe: [
"............",
"...######...",
"..#..#...#..",
".#...#....#.",
"############",
"#....#.....#",
"#....#.....#",
"############",
".#...#....#.",
"..#..#...#..",
"...######...",
"............",
],
disk: [
"............",
"############",
"#..######..#",
"#..#....#..#",
"#..#....#..#",
"#..######..#",
"#..........#",
"#.########.#",
"#.#......#.#",
"#.#......#.#",
"############",
"............",
],
folder: [
"............",
".####.......",
"#....#......",
"#.....######",
"#..........#",
"#..........#",
"#..........#",
"#..........#",
"#..........#",
"############",
"............",
"............",
],
};
function glyphSvg(name) {
const grid = GLYPHS[name] || GLYPHS.box;
const rects = [];
grid.forEach((row, y) => {
// Waagerechte Läufe zusammenfassen: weniger Rechtecke, gleiches Bild.
let run = 0;
for (let x = 0; x <= row.length; x++) {
if (row[x] === "#") {
run++;
} else if (run) {
rects.push(`<rect x="${x - run}" y="${y}" width="${run}" height="1"/>`);
run = 0;
}
}
});
return `<svg viewBox="0 0 12 12" fill="currentColor" shape-rendering="crispEdges" aria-hidden="true">${rects.join("")}</svg>`;
}
/* ── 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. */
const APPS_KEY = document.querySelector('meta[name="honeyos-key"]')?.content || "";
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;
}
function cookie(name) {
return document.cookie
.split(";")
.map((c) => c.trim().split("="))
.find(([k]) => k === name)?.[1] || "";
}
/* ── Laden & Speichern ─────────────────────────────────────────────────── */
@@ -336,46 +45,8 @@ async function loadFromServer() {
/** Einstellungen vorab aus localStorage, damit die Röhre nicht erst weiß
* aufblitzt und dann umspringt. */
function loadLocalSettings() {
state.settings = { ...DEFAULT_SETTINGS, ...(readJson(LS_SETTINGS) || {}) };
}
function readJson(key) {
try {
return JSON.parse(localStorage.getItem(key) || "null");
} catch {
return null;
}
}
let syncTimer = null;
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);
}
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;
}
/* ── Boot ──────────────────────────────────────────────────────────────── */
@@ -436,13 +107,6 @@ function runBoot() {
/* ── Gemeinsames: Menüleiste, Uhr, Stand, Start ────────────────────────── */
function greeting() {
const h = new Date().getHours();
if (h < 5 || h >= 23) return "Noch wach?";
if (h < 11) return "Guten Morgen.";
if (h < 18) return "Willkommen zurück.";
return "Guten Abend.";
}
function renderShell() {
$("desktop").hidden = false;
@@ -497,99 +161,7 @@ function renderMode() {
paintProbes();
}
/* ── Wer angemeldet ist ────────────────────────────────────────────────────
*
* Stand früher rechts in der Menüleiste. Dort war es das einzige Stück, das
* die HoneyBar in den Apps nicht hat — beide sollen aber gleich aussehen, sonst
* merkt man beim Wechsel, dass es zwei verschiedene Leisten sind. Auf dem
* Schreibtisch ist ohnehin Platz dafür, und die Begrüßung gehört dazu.
*/
function userHtml() {
const bild = state.me && state.me.picture
? `<img class="uw-bild" src="${escapeAttr(state.me.picture)}" alt="" />`
: `<span class="uw-bild uw-leer">${glyphSvg("chip")}</span>`;
return `
<div class="uw">
${bild}
<div class="uw-text">
<b class="uw-name">${state.cfg.user}</b>
<span class="uw-gruss">${greeting()}</span>
<span class="uw-host">${state.cfg.user}@${state.cfg.hostname}</span>
</div>
</div>`;
}
function renderUser() {
const tile = $("userTile");
const desk = $("userDesk");
const desktopMode = state.settings.mode === "desktop";
const html = userHtml();
tile.hidden = desktopMode;
desk.hidden = !desktopMode;
tile.querySelector(".user-body").innerHTML = html;
desk.innerHTML = html;
}
/* ── Wetter ────────────────────────────────────────────────────────────────
*
* Der Server holt es und legt es zehn Minuten beiseite; hier wird nur gezeigt,
* was er hergibt. Kein Ort eingestellt oder Dienst nicht erreichbar: 204, und
* das Widget bleibt weg. Eine Zahl von gestern wäre schlechter als keine.
*/
async function loadWeather() {
if (!state.settings.weatherCity) {
state.weather = null;
renderWeather();
return;
}
try {
state.weather = await api("/api/weather");
} catch {
state.weather = null;
}
renderWeather();
}
function weatherHtml(w) {
return `
<div class="wx">
<span class="wx-glyph">${glyphSvg(w.glyph)}</span>
<div class="wx-text">
<b class="wx-temp">${Math.round(w.grad)}°</b>
<span class="wx-desc">${w.text}</span>
<span class="wx-city">${w.ort}</span>
</div>
</div>`;
}
function renderWeather() {
const w = state.weather;
const tile = $("wxTile");
const desk = $("wxDesk");
const bar = $("mbWx");
const desktopMode = state.settings.mode === "desktop";
// Das Widget: Fenster im Terminal-Modus, Schild auf dem Schreibtisch.
const alsWidget = state.settings.weatherDesk === "on" && w;
tile.hidden = !alsWidget || desktopMode;
desk.hidden = !alsWidget || !desktopMode;
if (alsWidget) {
const html = weatherHtml(w);
tile.querySelector(".wx-body").innerHTML = html;
desk.innerHTML = html;
}
// Und in der HoneyBar. Die Menüleiste hier ist zwar anderer Code als
// honeybar.js in den Apps, aber für den, der davorsitzt, dieselbe HoneyBar —
// ein Schalter, der nur woanders wirkt, wäre nicht zu erklären.
const inLeiste = state.settings.weatherBar === "on" && w;
bar.hidden = !inLeiste;
if (inLeiste) {
bar.innerHTML = `${glyphSvg(w.glyph)}<span class="mb-wx-temp">${Math.round(w.grad)}°</span>`;
bar.title = `${w.ort}: ${w.text}, ${Math.round(w.grad)}°`;
}
}
function setMode(mode) {
if (state.settings.mode === mode) return;
@@ -1219,12 +791,6 @@ function openAbout() {
/* ── Hilfsmittel ───────────────────────────────────────────────────────── */
function escapeHtml(s) {
return String(s).replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
}
function escapeAttr(s) {
return escapeHtml(s).replace(/"/g, "&quot;");
}
/* ── Startmenü ─────────────────────────────────────────────────────────── */
+44
View File
@@ -0,0 +1,44 @@
import { $, escapeAttr, state } from "../core.js";
import { glyphSvg } from "../glyphs.js";
function greeting() {
const h = new Date().getHours();
if (h < 5 || h >= 23) return "Noch wach?";
if (h < 11) return "Guten Morgen.";
if (h < 18) return "Willkommen zurück.";
return "Guten Abend.";
}
/* ── Wer angemeldet ist ────────────────────────────────────────────────────
*
* Stand früher rechts in der Menüleiste. Dort war es das einzige Stück, das
* die HoneyBar in den Apps nicht hat — beide sollen aber gleich aussehen, sonst
* merkt man beim Wechsel, dass es zwei verschiedene Leisten sind. Auf dem
* Schreibtisch ist ohnehin Platz dafür, und die Begrüßung gehört dazu.
*/
function userHtml() {
const bild = state.me && state.me.picture
? `<img class="uw-bild" src="${escapeAttr(state.me.picture)}" alt="" />`
: `<span class="uw-bild uw-leer">${glyphSvg("chip")}</span>`;
return `
<div class="uw">
${bild}
<div class="uw-text">
<b class="uw-name">${state.cfg.user}</b>
<span class="uw-gruss">${greeting()}</span>
<span class="uw-host">${state.cfg.user}@${state.cfg.hostname}</span>
</div>
</div>`;
}
export function renderUser() {
const tile = $("userTile");
const desk = $("userDesk");
const desktopMode = state.settings.mode === "desktop";
const html = userHtml();
tile.hidden = desktopMode;
desk.hidden = !desktopMode;
tile.querySelector(".user-body").innerHTML = html;
desk.innerHTML = html;
}
+62
View File
@@ -0,0 +1,62 @@
import { $, api, state } from "../core.js";
import { glyphSvg } from "../glyphs.js";
/* ── Wetter ────────────────────────────────────────────────────────────────
*
* Der Server holt es und legt es zehn Minuten beiseite; hier wird nur gezeigt,
* was er hergibt. Kein Ort eingestellt oder Dienst nicht erreichbar: 204, und
* das Widget bleibt weg. Eine Zahl von gestern wäre schlechter als keine.
*/
export async function loadWeather() {
if (!state.settings.weatherCity) {
state.weather = null;
renderWeather();
return;
}
try {
state.weather = await api("/api/weather");
} catch {
state.weather = null;
}
renderWeather();
}
function weatherHtml(w) {
return `
<div class="wx">
<span class="wx-glyph">${glyphSvg(w.glyph)}</span>
<div class="wx-text">
<b class="wx-temp">${Math.round(w.grad)}°</b>
<span class="wx-desc">${w.text}</span>
<span class="wx-city">${w.ort}</span>
</div>
</div>`;
}
export function renderWeather() {
const w = state.weather;
const tile = $("wxTile");
const desk = $("wxDesk");
const bar = $("mbWx");
const desktopMode = state.settings.mode === "desktop";
// Das Widget: Fenster im Terminal-Modus, Schild auf dem Schreibtisch.
const alsWidget = state.settings.weatherDesk === "on" && w;
tile.hidden = !alsWidget || desktopMode;
desk.hidden = !alsWidget || !desktopMode;
if (alsWidget) {
const html = weatherHtml(w);
tile.querySelector(".wx-body").innerHTML = html;
desk.innerHTML = html;
}
// Und in der HoneyBar. Die Menüleiste hier ist zwar anderer Code als
// honeybar.js in den Apps, aber für den, der davorsitzt, dieselbe HoneyBar —
// ein Schalter, der nur woanders wirkt, wäre nicht zu erklären.
const inLeiste = state.settings.weatherBar === "on" && w;
bar.hidden = !inLeiste;
if (inLeiste) {
bar.innerHTML = `${glyphSvg(w.glyph)}<span class="mb-wx-temp">${Math.round(w.grad)}°</span>`;
bar.title = `${w.ort}: ${w.text}, ${Math.round(w.grad)}°`;
}
}