Systemmenue im Logo, in beiden Leisten

Das ■ HONEYOS ist jetzt ein Menue statt eines Links: Schreibtisch,
Einstellungen, HoneyBar einklappen, Abmelden. In fremden Apps sind es
Links auf den Schreibtisch (?einstellungen=1 / ?abmelden=1), weil die
HoneyBar dort nicht schreiben darf — Sitzung und CSRF-Pruefwort ueber-
leben die Site-Grenze nicht, und ein Abmelden per Aufruf koennte jede
Seite ausloesen.

Der Abmeldeknopf in der Menueleiste faellt weg (stand doppelt), und
Abmelden erscheint nur, wenn HoneyOS ueberhaupt anmeldet — dafuer gibt
/api/apps jetzt oidc mit heraus. Nebenbei: restliche 'Leiste' im Text
zu 'HoneyBar' vereinheitlicht.
This commit is contained in:
marc committed 2026-08-29 21:23:11 +02:00
1 parent 52deffa30e
commit 0a5cd94ba0
12 files changed
+244 -51

No files matched your search

+18 -6
View File
@@ -30,14 +30,26 @@ Offen bleibt: Bei mehreren Nutzern trägt die HoneyBar die Farbe des
Standardnutzers, weil ihr über die Site-Grenze niemand sagen kann, wer da
gerade schaut.
### 1.2 Systemmenü im Logo
### 1.2 Systemmenü im Logo — ✅ gebaut (29.08.2026)
Das `■ HONEYOS` links ist heute nur ein Link zum Schreibtisch. Wie ein
Apfel-Menü gehören darunter: Schreibtisch · Einstellungen · Abmelden. Dann sind
die HoneyOS-Sachen aus jeder App heraus erreichbar, ohne sie zu verlassen.
**Steht**, in beiden Leisten mit denselben Punkten (siehe [README](README.md)).
Der Abmeldeknopf in der Menüleiste des Schreibtischs ist dafür weggefallen — er
stand sonst zweimal da, und die HoneyBar hat ihn nie gehabt.
Die Einstellungen sind derselbe Inhalt wie auf dem Schreibtisch — sie liegen im
Server, also stimmt beides automatisch überein.
Eine Annahme von damals hat sich nicht gehalten: „ohne sie zu verlassen"
**geht nicht**, jedenfalls nicht für Einstellungen und Abmelden. Beides
schreibt, und die HoneyBar darf in einer fremden App nicht schreiben — die
Sitzung kommt über die Site-Grenze nicht mit, und selbst unter einer Domain
liegt das CSRF-Prüfwort in einem Cookie, das nur HoneyOS' eigene Seiten lesen.
Also reicht die HoneyBar die Frage als `?einstellungen=1` / `?abmelden=1` an den
Schreibtisch weiter. Ein Klick, ein Seitenwechsel — dort, wo man angemeldet ist.
**Was ohne Seitenwechsel ginge, wenn es je stört:** die reinen Aussehensschalter
(Röhre, Leuchten, Scanlines) direkt im Menü. Sie kämen ohne Prüfwort aus, wenn
der Server stattdessen den `Origin` der Anfrage gegen die eingetragenen Apps
hält — dieselbe Liste, die heute schon die CORS-Freigabe steuert. Das ist eine
Sicherheitsentscheidung und keine Kleinigkeit; erst bauen, wenn der Umweg über
den Schreibtisch wirklich nervt.
### 1.3 App-Schublade mit Symbolen
+30 -1
View File
@@ -386,12 +386,41 @@ nur, wessen App selbst eine schickt.
- **Andere Proxys:** Caddy braucht ein Zusatzmodul (`replace-response`),
Traefik ein Plugin für den Antwortkörper. nginx kann es ohne Erweiterung.
## Das Systemmenü im Logo
Das `■ HONEYOS` links ist ein Menü — auf dem Schreibtisch wie in jeder App:
| in der HoneyBar (fremde App) | auf dem Schreibtisch |
|---|---|
| Schreibtisch | Über HoneyOS |
| Einstellungen … | Einstellungen |
| HoneyBar einklappen | Terminal-/Desktop-Modus |
| Abmelden | Abmelden |
Rechts daneben bleibt der Pfeil mit den Apps. Eines auf heißt anderes zu.
**In den Apps sind es Links auf den Schreibtisch, keine Handlungen** — und das
ist eine Grenze, keine Bequemlichkeit. Einstellungen und Abmelden schreiben, und
dafür braucht der Server die HoneyOS-Sitzung: Aus einer fremden Site schickt der
Browser sie nicht mit, und selbst unter einer Domain liegt das CSRF-Prüfwort in
einem Cookie, das nur HoneyOS' eigene Seiten lesen dürfen. Ein Abmelden, das ein
einfacher Aufruf erledigt, könnte außerdem jede beliebige Seite auslösen.
Also stellt die HoneyBar die Frage in der Adresse (`?einstellungen=1`,
`?abmelden=1`), und der Schreibtisch beantwortet sie: Er macht dasselbe
Einstellungsfeld auf, das auch `[S]` öffnet, und meldet mit einem echten POST
ab. Danach nimmt er den Anhang aus der Adresse wieder heraus.
`Abmelden` steht nur da, wenn HoneyOS überhaupt anmeldet — im offenen Betrieb
oder hinter einem Proxy fehlt der Punkt. Deshalb gibt `/api/apps` neben Liste
und Aussehen auch `oidc` heraus.
## Wer angemeldet ist
Steht auf dem Schreibtisch, als eigenes Widget: Bild, Name und Begrüßung, aus
der HoneyOS-Anmeldung. In den Apps steht es **nicht** — die HoneyBar dort trägt
genau dasselbe wie die Menüleiste hier, sonst merkt man beim Wechsel, dass es
zwei verschiedene Leisten sind.
zwei verschiedene HoneyBars sind.
Früher konnte jede App einen „Wer-bin-ich"-Pfad und einen Token-Schlüssel
mitbringen; die HoneyBar fragte damit die App nach dem Anzeigenamen. Beides ist
+3 -3
View File
@@ -213,10 +213,10 @@ func (s *Store) SetSettings(sub string, in map[string]string) error {
// SettingsFor liefert das Aussehen für sub — und hilft nach, wenn die Frage
// über eine Site-Grenze kam.
//
// Die Leiste in einer fremden App weist sich nur mit dem Schlüssel aus, nicht
// Die HoneyBar in einer fremden App weist sich nur mit dem Schlüssel aus, nicht
// mit einer Sitzung: der Browser schickt das Cookie dorthin nicht mit. Sie
// landet deshalb beim Standardnutzer. Sind die Einstellungen aber unter einem
// Namen aus der Proxy-Kopfzeile gespeichert, wäre der leer — und die Leiste
// Namen aus der Proxy-Kopfzeile gespeichert, wäre der leer — und die HoneyBar
// bliebe bernsteinfarben, während der Schreibtisch grün leuchtet.
//
// Gibt es genau einen Nutzer mit Einstellungen, gilt dessen Aussehen. Bei
@@ -246,7 +246,7 @@ func (s *Store) SettingsFor(sub string) map[string]string {
}
// AppOrigins nennt die Ursprünge der eingetragenen Apps. Nur sie dürfen die
// Leiste mit Anmeldedaten abfragen — die Liste ist damit ihre eigene Freigabe.
// HoneyBar mit Anmeldedaten abfragen — die Liste ist damit ihre eigene Freigabe.
func (s *Store) AppOrigins() map[string]bool {
s.mu.RLock()
defer s.mu.RUnlock()
+1 -1
View File
@@ -16,7 +16,7 @@ import (
// Wetter kommt vom Server, nicht aus dem Browser.
//
// Die Leiste läuft auf jeder Seite jeder App. Fragte sie selbst beim
// Die HoneyBar läuft auf jeder Seite jeder App. Fragte sie selbst beim
// Wetterdienst an, ginge bei jedem Seitenaufruf aus jeder App eine Anfrage samt
// IP dorthin — und die CSP jeder App müsste den fremden Ursprung erlauben. Holt
// HoneyOS es, sieht der Dienst einen Server, und die Apps bleiben unberührt.
+1 -1
View File
@@ -52,7 +52,7 @@ func TestWetterKommtAn(t *testing.T) {
}
}
// Der Zwischenspeicher ist der Grund, warum die Leiste auf jeder Seite jeder App
// Der Zwischenspeicher ist der Grund, warum die HoneyBar auf jeder Seite jeder App
// stehen darf: Ohne ihn ginge bei jedem Seitenaufruf eine Anfrage nach draußen.
func TestZweiteFrageGehtNichtNachDraussen(t *testing.T) {
var aufrufe int32
+12 -9
View File
@@ -31,7 +31,7 @@ var webFS embed.FS
// Version ist die einzige Stelle, an der sie steht. build.sh liest sie hier aus
// und vergibt denselben Docker-Tag; die Oberfläche holt sie über /api/public.
// Vorher stand sie zusätzlich in os.js und lief mit den Image-Tags auseinander.
const Version = "1.0"
const Version = "1.1"
type Server struct {
cfg config.Config
@@ -128,7 +128,7 @@ func (s *Server) routes() http.Handler {
mux.HandleFunc("PUT /api/settings", s.requireAuth(s.handleSetSettings))
mux.HandleFunc("GET /api/apps", s.appsReadable(s.handleApps))
mux.HandleFunc("PUT /api/apps", s.requireAuth(s.handleSetApps))
// Dieselbe Tür wie die App-Liste: Die Leiste steht in einer fremden App und
// Dieselbe Tür wie die App-Liste: Die HoneyBar steht in einer fremden App und
// kommt dort nur mit dem Schlüssel an Daten.
mux.HandleFunc("GET /api/weather", s.appsReadable(s.handleWeather))
// Die Ortssuche dagegen nur für Angemeldete. Sie reicht einen Suchbegriff an
@@ -180,7 +180,7 @@ func htmlAttr(v string) string {
/* ── Zwischenschichten ───────────────────────────────────────────────────── */
// cors öffnet die Leiste für die eingetragenen Apps — und nur für die. Die
// cors öffnet die HoneyBar für die eingetragenen Apps — und nur für die. Die
// App-Liste ist damit zugleich die Freigabeliste; wer nicht darin steht, kommt
// auch nicht an die Sitzung.
func (s *Server) cors(next http.Handler) http.Handler {
@@ -205,11 +205,11 @@ func (s *Server) cors(next http.Handler) http.Handler {
func noCache(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache")
// Die Leiste wird von fremden Ursprüngen geladen — das ist ihr Zweck.
// Die HoneyBar wird von fremden Ursprüngen geladen — das ist ihr Zweck.
// Apps, die selbst COEP „require-corp" setzen (alles mit WASM oder
// SharedArrayBuffer, etwa BentoPDF), lehnen sonst jede fremde Datei ab,
// die sich nicht ausdrücklich dazu bekennt. Ohne diese Zeile erscheint
// dort keine Leiste, und im Fehlerprotokoll steht nur ein Kürzel.
// dort keine HoneyBar, und im Fehlerprotokoll steht nur ein Kürzel.
w.Header().Set("Cross-Origin-Resource-Policy", "cross-origin")
next.ServeHTTP(w, r)
})
@@ -220,7 +220,7 @@ func noCache(next http.Handler) http.Handler {
// Der Grund ist eine Browserregel, keine Bequemlichkeit: Liegt HoneyOS auf
// einer anderen Site als die Apps, schickt der Browser die Sitzung bei der
// Anfrage aus einer App heraus nicht mit. Ohne einen zweiten Ausweis bekäme die
// Leiste dort nie eine Liste. Der Schlüssel steht im Skript-Tag der App und ist
// HoneyBar dort nie eine Liste. Der Schlüssel steht im Skript-Tag der App und ist
// damit nur für den lesbar, der ohnehin an die App herankommt.
func (s *Server) appsReadable(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
@@ -305,24 +305,27 @@ func (s *Server) handleSetSettings(w http.ResponseWriter, r *http.Request, sub s
}
// handleApps liefert die Liste **und** das Aussehen. Beides zusammen, weil die
// Leiste in einer fremden App genau eine Anfrage stellen kann: Sie liegt oft auf
// HoneyBar in einer fremden App genau eine Anfrage stellen kann: Sie liegt oft auf
// einer anderen Site, kommt also nur mit dem Schlüssel an Daten und hat keine
// Sitzung, aus der sich der Rest ergäbe.
//
// Welche Einstellungen? Die des Angemeldeten, wenn es einen gibt — sonst die des
// Standardnutzers. Bei mehreren Nutzern trägt die Leiste damit die Farbe des
// Standardnutzers. Bei mehreren Nutzern trägt die HoneyBar damit die Farbe des
// Standardnutzers; für eine Person stimmt es immer.
func (s *Server) handleApps(w http.ResponseWriter, r *http.Request, sub string) {
writeJSON(w, http.StatusOK, map[string]any{
"apps": s.store.Apps(),
"settings": s.store.SettingsFor(sub),
// Fuer das Systemmenue der HoneyBar: ohne Anbieter gibt es nichts
// abzumelden, dann steht der Punkt dort auch nicht.
"oidc": s.auth.Enabled(),
})
}
// handleWeather liefert das Wetter für den eingestellten Ort — oder nichts.
//
// „Nichts" ist hier ein 204 und kein Fehler: Kein Ort eingestellt ist der
// Normalfall, solange niemand einen gewählt hat, und die Leiste soll dafür
// Normalfall, solange niemand einen gewählt hat, und die HoneyBar soll dafür
// nicht rot blinken, sondern das Symbol einfach weglassen. Dasselbe gilt, wenn
// der Dienst nicht erreichbar ist: lieber kein Wetter als eine Zahl von gestern.
func (s *Server) handleWeather(w http.ResponseWriter, r *http.Request, sub string) {
+10 -2
View File
@@ -769,6 +769,14 @@ body {
box-shadow: 0 -8px 30px rgba(0, 0, 0, 0.6);
}
.startmenu[data-open="1"] { display: flex; }
/* Dasselbe Menü, nur andersherum aufgehängt: das Startmenü wächst aus der
Taskleiste nach oben, das Systemmenü aus der Menüleiste nach unten. */
.sysmenu {
top: calc(32px + env(safe-area-inset-top) + 2px);
bottom: auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
}
.mb-logo[aria-expanded="true"] { background: var(--p-faint); }
.startmenu .sm-head {
padding: 4px 8px;
font-size: 10px;
@@ -846,7 +854,7 @@ body {
}
.login-note b { color: var(--p-mid); font-weight: 400; }
/* Nutzer in der Leiste */
/* Nutzer in der HoneyBar */
/* ── App-Verwaltung ────────────────────────────────────────────────────── */
.apped { display: flex; flex-direction: column; gap: 10px; }
@@ -980,7 +988,7 @@ body {
.wx-hit:hover { border-color: var(--p-faint); color: var(--p); }
/* Wetter in der Menüleiste — dasselbe Maß wie in honeybar.js, damit die
Leiste hier und die in den Apps nicht unterschiedlich aussehen. */
HoneyBar hier und die in den Apps nicht unterschiedlich aussehen. */
.mb-wx {
display: flex;
align-items: center;
+80 -16
View File
@@ -56,6 +56,8 @@
let apps = DEFAULT_APPS;
let settings = null;
// Ob HoneyOS ueberhaupt anmeldet — ohne Anbieter gaebe es nichts abzumelden.
let oidc = false;
// Was die Seite oben schon hatte, bevor die HoneyBar sie heruntergeschoben hat.
let urspruenglichesPolster = null;
@@ -149,6 +151,7 @@
wie ein anderes Logo aus. */
.home .hex { font-size: 10px; vertical-align: 2px; }
.home:hover { border-color: var(--p-dim); }
.home[aria-expanded="true"] { border-color: var(--p-dim); background: var(--p-faint); }
/* Bei vielen Apps schiebt man den Streifen seitwärts. Ohne
flex-shrink:0 an den Knöpfen quetscht das Raster sie stattdessen
zusammen, bis nur noch Buchstabenreste dastehen — am Handy sofort.
@@ -183,6 +186,11 @@
background: var(--bg); border: 1px solid var(--p-dim); padding: 4px;
display: none; flex-direction: column; gap: 2px;
box-shadow: 0 10px 30px rgba(0,0,0,.6);
/* Die Menues haengen neben der HoneyBar, nicht darin — ohne eigene
Schrift erben sie die der fremden App und stehen dann in deren
Fliesstext da. Genau so sah es aus. */
font: 12px/1.2 ui-monospace, "SF Mono", "DejaVu Sans Mono", Menlo, Consolas, monospace;
color: var(--p); text-shadow: var(--glow);
}
.menu[data-open="1"] { display: flex; }
.menu a, .menu button { text-align: left; padding: 6px 8px; color: var(--p-mid); }
@@ -192,11 +200,14 @@
padding: 4px 8px; font-size: 10px; letter-spacing: .18em;
text-transform: uppercase; color: var(--p-dim);
}
/* Dasselbe Menue, nur an der anderen Seite: das Systemmenue haengt am
Logo links, das App-Menue am Pfeil rechts. */
.menu.sys { left: 6px; right: auto; }
@media (max-width: 560px) { .clock { display: none; } }
</style>
<div class="bar">
<a class="home" id="home" title="HoneyOS"><span class="hex">&#9632;</span> HONEY<b>OS</b></a>
<button class="home" id="home" title="HoneyOS-Menü" aria-haspopup="true" aria-expanded="false"><span class="hex">&#9632;</span> HONEY<b>OS</b></button>
<nav class="apps" id="apps"></nav>
<div class="right">
<span class="wx" id="wx" hidden></span>
@@ -205,11 +216,19 @@
</div>
</div>
<div class="menu sys" id="sysmenu" role="menu">
<div class="head">honeyos</div>
<a id="mHome" role="menuitem">Schreibtisch</a>
<a id="mSettings" role="menuitem">Einstellungen &hellip;</a>
<hr />
<button id="hide" role="menuitem">HoneyBar einklappen</button>
<hr id="mLogoutSep" hidden />
<a id="mLogout" role="menuitem" hidden>Abmelden</a>
</div>
<div class="menu" id="menu" role="menu">
<div class="head">wechseln zu</div>
<div id="menuApps"></div>
<hr />
<button id="hide" role="menuitem">honeybar einklappen</button>
</div>`;
/* ── Aussehen ─────────────────────────────────────────────────────────────
@@ -327,30 +346,46 @@
clock();
setInterval(clock, 20000);
root.getElementById("home").href = homeUrl;
systemZiele();
}
function verdrahten() {
if (verdrahtet) return;
verdrahtet = true;
const menu = root.getElementById("menu");
const btn = root.getElementById("menuBtn");
btn.addEventListener("click", (e) => {
e.stopPropagation();
const open = menu.dataset.open === "1";
menu.dataset.open = open ? "0" : "1";
btn.setAttribute("aria-expanded", String(!open));
// Zwei Menues, gleiche Mechanik: links das System, rechts die Apps.
// Eines auf heisst anderes zu — zwei offene Felder in 32 px Hoehe
// ueberdecken sich gegenseitig.
const paare = [
[root.getElementById("sysmenu"), root.getElementById("home")],
[root.getElementById("menu"), root.getElementById("menuBtn")],
];
const close = () => {
paare.forEach(([m, b]) => {
m.dataset.open = "0";
b.setAttribute("aria-expanded", "false");
});
};
paare.forEach(([menu, btn]) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const auf = menu.dataset.open === "1";
close();
if (!auf) {
menu.dataset.open = "1";
btn.setAttribute("aria-expanded", "true");
}
});
});
// Klick daneben schließt — das Ereignis muss die Grenze des Shadow DOM
// in beide Richtungen überleben, deshalb beide Wurzeln.
const close = () => {
menu.dataset.open = "0";
btn.setAttribute("aria-expanded", "false");
};
document.addEventListener("click", close);
root.addEventListener("click", (e) => {
if (!e.composedPath().includes(menu) && e.target !== btn) close();
const drin = paare.some(([m, b]) => e.composedPath().includes(m) || e.target === b);
if (!drin) close();
});
root.getElementById("hide").addEventListener("click", () => {
@@ -364,6 +399,34 @@
});
}
/**
* Die drei Wege ins System. Alle drei sind Links auf den Schreibtisch, keine
* Aktionen hier — und das ist keine Bequemlichkeit, sondern die Grenze:
*
* Einstellungen und Abmelden **schreiben**. Dafuer braucht der Server die
* HoneyOS-Sitzung, und die schickt der Browser aus einer fremden App nicht
* mit; liegt alles unter einer Domain, kommt sie zwar mit, aber das
* CSRF-Prüfwort steht in einem Cookie der HoneyOS-Adresse, das dieses Skript
* von hier aus nicht lesen darf. Und ein Abmelden, das ein simpler Aufruf
* erledigt, koennte jede fremde Seite ausloesen.
*
* Also: die HoneyBar zeigt den Weg, gehandelt wird dort, wo man angemeldet
* ist. Der Schreibtisch sieht die Frage in der Adresse und macht sie auf.
*/
function systemZiele() {
const ziel = (q) => {
const u = new URL(homeUrl, location.href);
u.search = q;
return u.href;
};
root.getElementById("mHome").href = new URL(homeUrl, location.href).href;
root.getElementById("mSettings").href = ziel("einstellungen=1");
const ab = root.getElementById("mLogout");
ab.href = ziel("abmelden=1");
ab.hidden = !oidc;
root.getElementById("mLogoutSep").hidden = !oidc;
}
/**
* Eingeklappt bleibt ein 3 px hoher Strich am oberen Rand stehen. Er nimmt
* praktisch keinen Platz, liegt über dem Inhalt statt ihn zu schieben — und
@@ -457,6 +520,7 @@
apps = [{ id: "honeyos", name: "DESKTOP", url: homeUrl }].concat(data.apps);
}
settings = data.settings || null;
oidc = !!data.oidc;
}
} catch {
/* nicht angemeldet, kein CORS, kein Netz → eingebaute Liste;
+3 -2
View File
@@ -30,7 +30,9 @@
<div class="desktop" id="desktop" hidden>
<!-- Menüleiste: dieselbe Anatomie wie honeybar.js in den Apps -->
<header class="menubar">
<button class="mb-logo" id="btnAbout" title="Über HoneyOS">
<!-- Das Logo ist das Systemmenü — dasselbe wie in der HoneyBar
fremder Apps, damit HoneyOS von überall gleich erreichbar ist. -->
<button class="mb-logo" id="btnSystem" title="HoneyOS-Menü" aria-haspopup="true" aria-expanded="false">
<span class="hex">&#9632;</span> HONEY<b>OS</b>
</button>
<nav class="mb-apps" id="mbApps" aria-label="Anwendungen"></nav>
@@ -38,7 +40,6 @@
<span class="mb-wx" id="mbWx" hidden></span>
<span class="mb-clock" id="clock">--:--</span>
<button class="mb-btn" id="btnSettings" title="Einstellungen (S)">&#9881;</button>
<button class="mb-btn" id="btnLogout" title="Abmelden" hidden>&#9211;</button>
</div>
</header>
+82 -6
View File
@@ -110,7 +110,6 @@ function runBoot() {
function renderShell() {
$("desktop").hidden = false;
$("btnLogout").hidden = !state.oidc;
renderMenubarApps();
renderMode();
tickClock();
@@ -792,6 +791,60 @@ function openAbout() {
/* ── Hilfsmittel ───────────────────────────────────────────────────────── */
/* ── Systemmenü im Logo ────────────────────────────────────────────────────
* Der Zwilling des Menüs in honeybar.js: gleiche Punkte, gleiche Reihenfolge.
* Hier sind es Handlungen, dort Links hierher — denn nur hier ist man
* angemeldet. Wer aus Aether auf „Einstellungen“ klickt, landet auf genau
* demselben Feld, das dieser Punkt aufmacht.
*/
function buildSystemMenu() {
const menu = document.createElement("div");
menu.className = "startmenu sysmenu";
menu.id = "sysmenu";
$("desktop").appendChild(menu);
const btn = $("btnSystem");
btn.addEventListener("click", (e) => {
e.stopPropagation();
if (menu.dataset.open === "1") return closeSystemMenu();
closeStartMenu();
const terminal = state.settings.mode === "terminal";
menu.innerHTML =
`<div class="sm-head">honeyos</div>
<button data-do="about">Über HoneyOS</button>
<button data-do="settings">Einstellungen</button>
<hr />
<button data-do="mode">${terminal ? "▣ Desktop-Modus" : "↩ Terminal-Modus"}</button>` +
(state.oidc ? `<hr /><button data-do="logout">⏻ Abmelden</button>` : "");
menu.querySelectorAll("[data-do]").forEach((b) =>
b.addEventListener("click", () => {
closeSystemMenu();
({
about: openAbout,
settings: openSettings,
mode: () => setMode(terminal ? "desktop" : "terminal"),
logout,
})[b.dataset.do]();
})
);
menu.dataset.open = "1";
btn.setAttribute("aria-expanded", "true");
});
document.addEventListener("click", closeSystemMenu);
}
function closeSystemMenu() {
const menu = $("sysmenu");
if (!menu) return;
menu.dataset.open = "0";
$("btnSystem").setAttribute("aria-expanded", "false");
}
/* ── Startmenü ─────────────────────────────────────────────────────────── */
function buildStartMenu() {
@@ -805,6 +858,10 @@ function buildStartMenu() {
e.stopPropagation();
const open = menu.dataset.open === "1";
if (open) return closeStartMenu();
// Beide Menüs offen ginge, sähe aber aus wie ein Fehler: Der Klick auf den
// Startknopf hält sein Ereignis auf, also erreicht es den Schließer des
// Systemmenüs nicht von selbst.
closeSystemMenu();
menu.innerHTML = `<div class="sm-head">starten</div>` +
state.cfg.apps.map((a) => `<button data-app="${a.id}">${escapeHtml(a.name)}</button>`).join("") +
@@ -812,8 +869,10 @@ function buildStartMenu() {
<button data-win="system">System</button>
<button data-win="settings">Einstellungen</button>
<button data-win="about">Über HoneyOS</button>
<hr /><button data-mode="terminal">↩ Terminal-Modus</button>
<button data-logout="1">⏻ Abmelden</button>`;
<hr /><button data-mode="terminal">↩ Terminal-Modus</button>` +
// Ohne Anbieter gibt es nichts abzumelden — im Systemmenü steht der
// Punkt aus demselben Grund nur bei Anmeldung.
(state.oidc ? `<button data-logout="1">⏻ Abmelden</button>` : "");
menu.querySelectorAll("[data-app]").forEach((b) =>
b.addEventListener("click", () => {
@@ -834,7 +893,7 @@ function buildStartMenu() {
closeStartMenu();
setMode("terminal");
});
menu.querySelector("[data-logout]").addEventListener("click", logout);
menu.querySelector("[data-logout]")?.addEventListener("click", logout);
menu.dataset.open = "1";
btn.setAttribute("aria-expanded", "true");
@@ -922,6 +981,13 @@ async function main() {
loadLocalSettings();
applySettings();
// ?abmelden=1 kommt aus dem Systemmenü der HoneyBar. Es hier zu erledigen
// statt am Server heißt: Die Abmeldung bleibt ein POST von dieser Seite aus,
// und keine fremde Seite kann sie durch ein eingebettetes Bild auslösen.
if (new URLSearchParams(location.search).get("abmelden") === "1") {
return logout();
}
let pub = { oidc: false, buttonLabel: "Anmelden" };
try {
pub = await api("/api/public");
@@ -948,18 +1014,26 @@ async function main() {
renderShell();
buildStartMenu();
buildSystemMenu();
probeApps();
setInterval(tickClock, 15000);
setInterval(probeApps, 120000);
$("btnSettings").addEventListener("click", openSettings);
$("btnAbout").addEventListener("click", openAbout);
$("btnLogout").addEventListener("click", logout);
$("btnClose").addEventListener("click", closeModal);
$("modal").addEventListener("click", (e) => {
if (e.target === $("modal")) closeModal();
});
// Was die HoneyBar aus einer fremden App herüberreicht: Sie darf selbst
// nichts schreiben (siehe honeybar.js), also stellt sie die Frage in der
// Adresse und der Schreibtisch beantwortet sie. Danach kommt die Adresse
// weg — ein Neuladen soll die Einstellungen nicht wieder aufreißen.
if (new URLSearchParams(location.search).get("einstellungen") === "1") {
history.replaceState(null, "", location.pathname);
openSettings();
}
window.addEventListener("keydown", (e) => {
if (e.metaKey || e.ctrlKey || e.altKey) return;
const typing = /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName);
@@ -968,6 +1042,8 @@ async function main() {
// einem gleichzeitig das oberste Fenster weg.
const menu = $("startmenu");
if (menu && menu.dataset.open === "1") return closeStartMenu();
const sys = $("sysmenu");
if (sys && sys.dataset.open === "1") return closeSystemMenu();
return state.settings.mode === "desktop" ? closeTopWindow() : closeModal();
}
if (typing) return;
+1 -1
View File
@@ -13,7 +13,7 @@ function greeting() {
*
* 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
* merkt man beim Wechsel, dass es zwei verschiedene HoneyBars sind. Auf dem
* Schreibtisch ist ohnehin Platz dafür, und die Begrüßung gehört dazu.
*/
function userHtml() {
+3 -3
View File
@@ -53,9 +53,9 @@ export function renderWeather() {
// 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) {
const inBar = state.settings.weatherBar === "on" && w;
bar.hidden = !inBar;
if (inBar) {
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)}°`;
}