Aus dem Quadrat links in beiden Leisten wird eine Pixelbiene von der Seite: braune Streifen, schwarze Beine, Fluegel mit Kerbe. Sie steht in genau einem Raster (tools/biene.py, 16x16 Zeichen); daraus entstehen SVG, PNG und eine Fassung aus Schriftzeichen. SVG statt PNG in den Leisten: Die HoneyBar laeuft in fremden Apps, und ob ein <img> dort laedt, entscheidet deren CSP. Markup kann niemand blockieren. Im System-Fenster und ueber dem Startlauf steht die Biene aus Schriftzeichen — beides sind Textflaechen, ein Bild darin waere ein Fremdkoerper. Das Wappen liegt an drei Stellen (Datei, Modul, HoneyBar-Zeile), weil die Leiste nichts nachladen kann. TestWappenIstUeberallDasselbe haelt sie gleich.
610 lines
28 KiB
JavaScript
610 lines
28 KiB
JavaScript
/*
|
|
* HoneyBar — die HoneyBar zum Einhängen in eine fremde App.
|
|
*
|
|
* <script src="https://os.honeyy.duckdns.org/honeybar/honeybar.js"
|
|
* data-app="aether"></script>
|
|
*
|
|
* Eine Zeile, kein Build, keine Abhängigkeit. Die HoneyBar rendert in einem
|
|
* Shadow DOM — die Stile der Gastgeber-App kommen nicht rein, unsere nicht
|
|
* raus. Das ist der ganze Grund für die Konstruktion: Aether ist React +
|
|
* Tailwind, InkNote React + eigene Palette, und beide sollen von der HoneyBar
|
|
* nichts merken außer den 32 Pixeln oben.
|
|
*
|
|
* data-Attribute am <script>:
|
|
* data-app (Pflicht) id der App, in der die HoneyBar steckt
|
|
* data-apps URL der App-Liste; ohne das /api/apps neben dem Skript
|
|
* data-key Schlüssel für die App-Liste. Nötig, wenn HoneyOS auf einer
|
|
* anderen Site liegt als die App: dann schickt der Browser die
|
|
* HoneyOS-Sitzung nicht mit, und ohne zweiten Ausweis bliebe die
|
|
* HoneyBar ohne Liste.
|
|
* data-home URL des HoneyOS-Desktops
|
|
* data-push "body" (Standard) schiebt den Seiteninhalt nach unten,
|
|
* "none" legt die HoneyBar darüber
|
|
*
|
|
* Wichtig zu wissen: localStorage ist pro Domain. Was auf os.honeyy… steht,
|
|
* sieht diese HoneyBar auf notes.honeyy… nicht — die HoneyBar ist ein Schalter,
|
|
* kein geteilter Zustand. Gemeinsame Sitzung wäre echtes SSO und ist eine
|
|
* eigene Baustelle.
|
|
*/
|
|
(function () {
|
|
"use strict";
|
|
|
|
if (window.__honeyBar) return; // zweimal eingebunden ist kein Fehler
|
|
window.__honeyBar = true;
|
|
|
|
const script = document.currentScript;
|
|
const d = (k, fb) => (script && script.dataset[k]) || fb;
|
|
|
|
const BAR_H = 32;
|
|
const LS_HIDDEN = "honeybar.hidden";
|
|
|
|
// Notliste, falls der Server nichts herausgibt: nur der Weg zurück zum
|
|
// Schreibtisch. Erfundene App-Adressen stünden hier sonst ewig fest.
|
|
const DEFAULT_APPS = [{ id: "honeyos", name: "DESKTOP", url: "" }];
|
|
|
|
const current = d("app", "");
|
|
// Ohne data-home führt der Weg zurück dorthin, wo dieses Skript herkommt.
|
|
const homeUrl = d("home", script ? new URL("/", script.src).href : "/");
|
|
const pushMode = d("push", "body");
|
|
// Ohne data-apps fragt die HoneyBar den HoneyOS-Server, von dem sie selbst
|
|
// geladen wurde. Er gibt die Liste nur an angemeldete Browser heraus und
|
|
// nur an Ursprünge, die selbst als App eingetragen sind.
|
|
const appsUrl = d("apps", script ? new URL("../api/apps", script.src).href : "");
|
|
const appsKey = d("key", "");
|
|
// Dieselbe Herkunft wie die App-Liste: Wo /api/apps liegt, liegt /api/weather.
|
|
const wetterUrl = appsUrl ? appsUrl.replace(/\/apps(\?|$)/, "/weather$1") : "";
|
|
|
|
let apps = DEFAULT_APPS;
|
|
let settings = null;
|
|
let glyphs = {};
|
|
// Was die Seite oben schon hatte, bevor die HoneyBar sie heruntergeschoben hat.
|
|
let urspruenglichesPolster = null;
|
|
|
|
/* ── Aufbau ───────────────────────────────────────────────────────────── */
|
|
|
|
// Als installierte App liegt der Inhalt unter der Statusleiste des Geraets
|
|
// (die Apps setzen viewport-fit=cover, damit der Hintergrund bis an den Rand
|
|
// reicht). Eine HoneyBar, die stur bei 0 anfaengt, liegt dort unter Uhrzeit und
|
|
// Akkustand. Deshalb waechst sie um die Aussparung und polstert sich selbst;
|
|
// im Browser ist env() null und alles bleibt bei 32 px.
|
|
const SAFE = "env(safe-area-inset-top, 0px)";
|
|
const HOEHE = "calc(" + BAR_H + "px + " + SAFE + ")";
|
|
|
|
/* Alle Pixelraster kommen mit der App-Liste vom Server (web/glyphs.json).
|
|
Frueher standen die Wettersymbole hier noch einmal abgetippt — genau das
|
|
war schon einmal auseinandergelaufen. Diese Datei bleibt trotzdem eine
|
|
einzelne ohne zweite Anfrage: Sie fragt ohnehin nach der App-Liste, und
|
|
die bringt die Raster einfach mit. */
|
|
function rasterSvg(grid) {
|
|
if (!grid) return "";
|
|
let d = "";
|
|
grid.forEach((row, y) => {
|
|
let run = 0;
|
|
for (let x = 0; x <= row.length; x++) {
|
|
if (row[x] === "#") run++;
|
|
else if (run) { d += `<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">${d}</svg>`;
|
|
}
|
|
|
|
const host = document.createElement("honey-bar");
|
|
host.style.cssText = [
|
|
"position:fixed", "top:0", "left:0", "right:0",
|
|
"height:" + HOEHE,
|
|
"padding-top:" + SAFE,
|
|
"box-sizing:border-box",
|
|
"z-index:2147483000",
|
|
"display:block",
|
|
"color-scheme:dark",
|
|
].join(";");
|
|
/* Das Wappen. Es steht hier als Markup, weil die HoneyBar in fremden
|
|
* Apps läuft: Ein <img> holte eine Datei von HoneyOS, und ob das geht,
|
|
* entscheidet die CSP der fremden App. Erzeugt von tools/biene.py —
|
|
* dieselbe Zeile steht in web/img/biene.svg (ein Test hält sie gleich).
|
|
*/
|
|
const WAPPEN = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" shape-rendering="crispEdges" aria-hidden="true"><rect x="4" y="0" width="2" height="1" fill="#f5f2e7"/><rect x="9" y="0" width="2" height="1" fill="#f5f2e7"/><rect x="2" y="1" width="5" height="1" fill="#f5f2e7"/><rect x="8" y="1" width="5" height="1" fill="#f5f2e7"/><rect x="2" y="2" width="11" height="1" fill="#f5f2e7"/><rect x="14" y="2" width="1" height="1" fill="#3b2412"/><rect x="3" y="3" width="9" height="1" fill="#f5f2e7"/><rect x="13" y="3" width="2" height="1" fill="#3b2412"/><rect x="3" y="4" width="11" height="1" fill="#ffd060"/><rect x="2" y="5" width="2" height="1" fill="#f2b33d"/><rect x="4" y="5" width="2" height="1" fill="#5c3a1c"/><rect x="6" y="5" width="2" height="1" fill="#f2b33d"/><rect x="8" y="5" width="2" height="1" fill="#5c3a1c"/><rect x="10" y="5" width="5" height="1" fill="#f2b33d"/><rect x="1" y="6" width="3" height="1" fill="#f2b33d"/><rect x="4" y="6" width="2" height="1" fill="#5c3a1c"/><rect x="6" y="6" width="2" height="1" fill="#f2b33d"/><rect x="8" y="6" width="2" height="1" fill="#5c3a1c"/><rect x="10" y="6" width="2" height="1" fill="#f2b33d"/><rect x="12" y="6" width="1" height="1" fill="#181410"/><rect x="13" y="6" width="1" height="1" fill="#63c7c0"/><rect x="14" y="6" width="1" height="1" fill="#f2b33d"/><rect x="0" y="7" width="1" height="1" fill="#5c3a1c"/><rect x="1" y="7" width="3" height="1" fill="#f2b33d"/><rect x="4" y="7" width="2" height="1" fill="#5c3a1c"/><rect x="6" y="7" width="2" height="1" fill="#f2b33d"/><rect x="8" y="7" width="2" height="1" fill="#5c3a1c"/><rect x="10" y="7" width="2" height="1" fill="#f2b33d"/><rect x="12" y="7" width="2" height="1" fill="#181410"/><rect x="14" y="7" width="1" height="1" fill="#f2b33d"/><rect x="1" y="8" width="3" height="1" fill="#f2b33d"/><rect x="4" y="8" width="2" height="1" fill="#5c3a1c"/><rect x="6" y="8" width="2" height="1" fill="#f2b33d"/><rect x="8" y="8" width="2" height="1" fill="#5c3a1c"/><rect x="10" y="8" width="5" height="1" fill="#f2b33d"/><rect x="2" y="9" width="1" height="1" fill="#d2952a"/><rect x="3" y="9" width="2" height="1" fill="#462b14"/><rect x="5" y="9" width="2" height="1" fill="#d2952a"/><rect x="7" y="9" width="2" height="1" fill="#462b14"/><rect x="9" y="9" width="6" height="1" fill="#d2952a"/><rect x="3" y="10" width="2" height="1" fill="#181410"/><rect x="7" y="10" width="2" height="1" fill="#181410"/><rect x="11" y="10" width="2" height="1" fill="#181410"/><rect x="3" y="11" width="2" height="1" fill="#181410"/><rect x="7" y="11" width="2" height="1" fill="#181410"/><rect x="11" y="11" width="2" height="1" fill="#181410"/></svg>';
|
|
|
|
const root = host.attachShadow({ mode: "open" });
|
|
|
|
root.innerHTML = `
|
|
<style>
|
|
:host, * { box-sizing: border-box; }
|
|
/* Dieselben Röhren wie auf dem Schreibtisch. Welche gilt, sagt der
|
|
Server; bis seine Antwort da ist, leuchtet Bernstein. */
|
|
:host {
|
|
--p: #ffb641;
|
|
--p-mid: #c8842a;
|
|
--p-dim: #6b4718;
|
|
--p-faint: #2a1c0c;
|
|
--bg: #140d07;
|
|
--glow: 0 0 5px rgba(255, 182, 65, .4);
|
|
--scan: rgba(0, 0, 0, .13);
|
|
}
|
|
.bar {
|
|
display: flex; align-items: center; gap: 10px;
|
|
height: ${BAR_H}px; padding: 0 8px;
|
|
background: var(--bg);
|
|
border-bottom: 1px solid var(--p-dim);
|
|
color: var(--p);
|
|
font: 12px/1 ui-monospace, "SF Mono", "DejaVu Sans Mono", Menlo, Consolas, monospace;
|
|
text-shadow: var(--glow);
|
|
user-select: none;
|
|
}
|
|
.bar::after {
|
|
/* Scanlines, damit die HoneyBar zum Desktop gehört */
|
|
content: ""; position: absolute; inset: 0; pointer-events: none;
|
|
background: repeating-linear-gradient(to bottom, var(--scan) 0 1px, transparent 1px 3px);
|
|
}
|
|
a, button {
|
|
font: inherit; color: inherit; background: none;
|
|
border: 1px solid transparent; cursor: pointer;
|
|
text-decoration: none; padding: 3px 9px; white-space: nowrap;
|
|
}
|
|
.home { letter-spacing: .14em; color: var(--p); padding-left: 2px; }
|
|
.home b { color: var(--p-mid); font-weight: 400; }
|
|
/* Dasselbe Wappen wie in der Menüleiste des Schreibtischs, in derselben
|
|
Größe. Es bleibt gelb, während der Rest die Röhre wechselt: Ein
|
|
Zeichen, das die Farbe mitwechselt, ist ein Symbol — eines, das seine
|
|
Farbe behält, ist ein Wappen. */
|
|
.home .hex { display: inline-block; width: 20px; height: 20px; vertical-align: -6px; }
|
|
.home .hex svg { display: block; width: 100%; height: 100%; }
|
|
.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.
|
|
Der Schatten rechts zeigt an, dass es weitergeht. */
|
|
/* Nur der Streifen in der Leiste. Wer eine weitere Klasse "apps"
|
|
vergibt, erbt hier Maske und Bildlauf mit. */
|
|
.apps {
|
|
display: flex; gap: 4px; flex: 1 1 auto; min-width: 0;
|
|
overflow-x: auto; scrollbar-width: none;
|
|
-webkit-overflow-scrolling: touch;
|
|
scroll-snap-type: x proximity;
|
|
mask-image: linear-gradient(90deg, #000 calc(100% - 18px), transparent);
|
|
}
|
|
.apps::-webkit-scrollbar { display: none; }
|
|
.app { color: var(--p-mid); letter-spacing: .08em; flex: 0 0 auto; scroll-snap-align: end; }
|
|
/* Symbol und Name in einer Zeile. Das Symbol traegt die Textfarbe, also
|
|
stimmt es in jeder Roehre und im umgekehrten Zustand von selbst. */
|
|
.app, .menu a[role="menuitem"] { display: inline-flex; align-items: center; gap: 6px; }
|
|
.ico { display: block; width: 12px; height: 12px; flex: 0 0 auto; }
|
|
.ico svg { width: 12px; height: 12px; display: block; }
|
|
.app:hover { color: var(--p); border-color: var(--p-dim); }
|
|
.app[aria-current="page"] {
|
|
background: var(--p); border-color: var(--p); color: var(--bg); text-shadow: none;
|
|
}
|
|
.right { display: flex; align-items: center; gap: 8px; }
|
|
.clock { color: var(--p-mid); font-variant-numeric: tabular-nums; }
|
|
/* Anzeige, kein Knopf: Wer darauf klickt, erwartet, dass etwas passiert.
|
|
Eingestellt wird das Wetter dort, wo alles eingestellt wird. */
|
|
.wx { display: flex; align-items: center; gap: 5px; color: var(--p-mid); }
|
|
.wx[hidden] { display: none; }
|
|
.wx svg { width: 14px; height: 14px; display: block; }
|
|
/* Am Handy nur das Symbol: 32 px Höhe und die Apps daneben lassen der
|
|
Temperatur keinen Platz mehr. */
|
|
@media (max-width: 700px) { .wx .grad { display: none; } }
|
|
.menu-btn { border-color: var(--p-dim); line-height: 1; padding: 3px 7px; }
|
|
.menu-btn:hover { background: var(--p-faint); }
|
|
.menu {
|
|
position: absolute; top: ${BAR_H + 2}px; right: 6px; min-width: 190px;
|
|
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; }
|
|
/* Die Listenpunkte des Systemmenues. Sie stehen VOR der Schublade:
|
|
Die Regel "menu a" ist spezifischer als die Klasse der Kachel, und
|
|
danach gewaehlt haetten die Kacheln Polster und Ausrichtung der Liste
|
|
geerbt statt ihrer eigenen. Genau das war passiert. */
|
|
.menu a, .menu button { text-align: left; padding: 6px 8px; color: var(--p-mid); }
|
|
.menu a:hover, .menu button:hover { background: var(--p-faint); color: var(--p); }
|
|
/* Die Schublade: Symbol, Name, Untertitel. Sie wird absichtlich
|
|
aufgemacht, weil man etwas sucht — also wird gelesen, nicht nur
|
|
wiedererkannt. Deshalb stehen hier immer Namen, anders als im
|
|
Streifen oben, wo ein Blick reichen muss. */
|
|
/* NICHT .menu.apps nennen: Der Streifen oben heisst .apps und blendet
|
|
seine rechten 18 px mit einer Maske aus, damit man sieht, dass es
|
|
seitwaerts weitergeht. Eine zweite Klasse "apps" am Menue holt diese
|
|
Maske mit — die Schublade sah rechts aus, als loese sie sich auf. */
|
|
.menu.schublade { min-width: 318px; max-width: min(360px, calc(100vw - 12px)); }
|
|
/* minmax(0, 1fr): Ohne das darf eine lange Beschreibung ihre Spalte
|
|
auseinanderdruecken, und die Schublade wird breiter als der Schirm. */
|
|
.drawer { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 2px; }
|
|
/* .menu .d-app, nicht .d-app: Die Kachel ist ein <a> im Menue, und
|
|
"menu a" oben ist spezifischer als eine einzelne Klasse — die
|
|
Kacheln trugen dadurch Polster und Ausrichtung der Listenpunkte.
|
|
Reihenfolge hilft dagegen nicht, nur Spezifitaet. */
|
|
.menu .d-app {
|
|
display: flex; flex-direction: column; align-items: center; gap: 5px;
|
|
padding: 9px 4px; text-align: center; color: var(--p-mid);
|
|
border: 1px solid transparent; min-width: 0;
|
|
}
|
|
/* Die Regel fuer a und button oben setzt nowrap — richtig fuer den
|
|
Streifen, falsch
|
|
hier: Der Untertitel soll umbrechen statt abgeschnitten zu werden. */
|
|
.menu .d-name, .menu .d-sub { max-width: 100%; }
|
|
.menu .d-sub { white-space: normal; }
|
|
.menu .d-name { overflow: hidden; text-overflow: ellipsis; }
|
|
.menu .d-app:hover { background: none; border-color: var(--p-dim); color: var(--p); }
|
|
.menu .d-app[aria-current="page"] { border-color: var(--p); color: var(--p); }
|
|
.menu .d-app .ico { width: 24px; height: 24px; }
|
|
.menu .d-app .ico svg { width: 24px; height: 24px; }
|
|
.menu .d-name { letter-spacing: .08em; }
|
|
/* Der Untertitel darf umbrechen, aber nicht wachsen — sonst stehen die
|
|
Kacheln unterschiedlich hoch. */
|
|
.menu .d-sub {
|
|
font-size: 10px; line-height: 1.25; color: var(--p-dim);
|
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
.menu hr { border: 0; border-top: 1px solid var(--p-faint); margin: 3px 0; }
|
|
.menu .head {
|
|
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">
|
|
<button class="home" id="home" title="HoneyOS-Menü" aria-haspopup="true" aria-expanded="false"><span class="hex">${WAPPEN}</span> HONEY<b>OS</b></button>
|
|
<nav class="apps" id="apps"></nav>
|
|
<div class="right">
|
|
<span class="wx" id="wx" hidden></span>
|
|
<span class="clock" id="clock"></span>
|
|
<button class="menu-btn" id="menuBtn" aria-haspopup="true" aria-expanded="false">▾</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="menu sys" id="sysmenu" role="menu">
|
|
<div class="head">honeyos</div>
|
|
<a id="mHome" role="menuitem">Schreibtisch</a>
|
|
<hr />
|
|
<button id="hide" role="menuitem">HoneyBar einklappen</button>
|
|
</div>
|
|
|
|
<div class="menu schublade" id="menu" role="menu">
|
|
<div class="head">wechseln zu</div>
|
|
<div class="drawer" id="menuApps"></div>
|
|
</div>`;
|
|
|
|
/* ── Aussehen ─────────────────────────────────────────────────────────────
|
|
* Dieselben drei Röhren wie in style.css. Sie hier zu wiederholen ist die
|
|
* bewusste Entscheidung: die HoneyBar soll ohne zweite Datei auskommen, damit
|
|
* sie in jeder fremden App mit einer Zeile funktioniert.
|
|
*/
|
|
const ROEHREN = {
|
|
amber: { p: "#ffb641", mid: "#c8842a", dim: "#6b4718", faint: "#2a1c0c", bg: "#140d07", glow: "255,182,65" },
|
|
green: { p: "#5cff8f", mid: "#2fa85c", dim: "#17512c", faint: "#08200f", bg: "#071206", glow: "92,255,143" },
|
|
ice: { p: "#cfe8ff", mid: "#7fa6c4", dim: "#395366", faint: "#12202a", bg: "#0a1118", glow: "207,232,255" },
|
|
};
|
|
|
|
function applySettings(s) {
|
|
if (!s) return;
|
|
const t = ROEHREN[s.phosphor] || ROEHREN.amber;
|
|
const bar = root.querySelector(".bar");
|
|
if (!bar) return;
|
|
const set = (k, v) => bar.parentNode.host.style.setProperty(k, v);
|
|
set("--p", t.p);
|
|
set("--p-mid", t.mid);
|
|
set("--p-dim", t.dim);
|
|
set("--p-faint", t.faint);
|
|
set("--bg", t.bg);
|
|
set("--glow", s.glow === "off" ? "none" : `0 0 5px rgba(${t.glow},.4)`);
|
|
set("--scan", s.scanlines === "off" ? "transparent" : "rgba(0,0,0,.13)");
|
|
}
|
|
|
|
/* ── Verhalten ────────────────────────────────────────────────────────── */
|
|
|
|
function render() {
|
|
const nav = root.getElementById("apps");
|
|
const menuApps = root.getElementById("menuApps");
|
|
nav.innerHTML = "";
|
|
menuApps.innerHTML = "";
|
|
|
|
// Der Schalter gilt nur fuer den Streifen. Im Aufklappmenue stehen die
|
|
// Symbole immer: Dort ist Platz, und dort sucht man eine App, statt sie
|
|
// im Vorbeigehen wiederzuerkennen.
|
|
const symboleImStreifen = !settings || settings.barGlyphs !== "off";
|
|
|
|
const symbol = (app) => {
|
|
const grid = glyphs[app.glyph] || glyphs.box;
|
|
if (!grid) return null;
|
|
const span = document.createElement("span");
|
|
span.className = "ico";
|
|
span.innerHTML = rasterSvg(grid);
|
|
return span;
|
|
};
|
|
|
|
apps.forEach((app) => {
|
|
if (app.id === "honeyos") return; // steckt schon links im Logo
|
|
|
|
const a = document.createElement("a");
|
|
a.className = "app";
|
|
a.href = app.url;
|
|
if (symboleImStreifen) {
|
|
const ico = symbol(app);
|
|
if (ico) a.appendChild(ico);
|
|
}
|
|
a.appendChild(document.createTextNode(app.name));
|
|
if (app.id === current) a.setAttribute("aria-current", "page");
|
|
nav.appendChild(a);
|
|
|
|
const m = document.createElement("a");
|
|
m.className = "d-app";
|
|
m.href = app.url;
|
|
m.setAttribute("role", "menuitem");
|
|
const ico = symbol(app);
|
|
if (ico) m.appendChild(ico);
|
|
const name = document.createElement("span");
|
|
name.className = "d-name";
|
|
name.textContent = app.name;
|
|
m.appendChild(name);
|
|
if (app.subtitle) {
|
|
const sub = document.createElement("span");
|
|
sub.className = "d-sub";
|
|
sub.textContent = app.subtitle;
|
|
m.appendChild(sub);
|
|
}
|
|
if (app.id === current) m.setAttribute("aria-current", "page");
|
|
menuApps.appendChild(m);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Das Wetter neben der Uhr — nur wenn in HoneyOS eingeschaltet und ein Ort
|
|
* eingestellt ist. Der Server antwortet sonst mit 204, und dann bleibt hier
|
|
* schlicht nichts stehen: lieber kein Symbol als eines ohne Aussage.
|
|
*/
|
|
async function wetter() {
|
|
if (!settings || settings.weatherBar !== "on") return;
|
|
let w;
|
|
try {
|
|
if (!wetterUrl) return;
|
|
const res = await fetch(wetterUrl, {
|
|
cache: "no-store",
|
|
credentials: "include",
|
|
headers: appsKey ? { "X-HoneyOS-Key": appsKey } : {},
|
|
});
|
|
if (res.status !== 200) return;
|
|
w = await res.json();
|
|
} catch {
|
|
return; // offline oder HoneyOS aus — die App geht das nichts an
|
|
}
|
|
const el = root.getElementById("wx");
|
|
el.innerHTML = rasterSvg(glyphs[w.glyph] || glyphs.wolke) + `<span class="grad">${Math.round(w.grad)}°</span>`;
|
|
el.title = `${w.ort}: ${w.text}, ${Math.round(w.grad)}°`;
|
|
el.hidden = false;
|
|
}
|
|
|
|
function clock() {
|
|
root.getElementById("clock").textContent = new Date().toLocaleTimeString("de-DE", {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
}
|
|
|
|
// Einmal verdrahtet, nicht bei jedem Einblenden neu.
|
|
//
|
|
// Der Fehler dahinter: mount() hing seine Zuhörer selbst an. Wer die HoneyBar
|
|
// einklappte und über den Strich zurückholte, hatte sie doppelt — der Klick
|
|
// auf das Menü öffnete und schloss es im selben Zug, und der Punkt
|
|
// „honeybar einklappen" war nicht mehr erreichbar.
|
|
let verdrahtet = false;
|
|
|
|
function mount() {
|
|
document.body.appendChild(host);
|
|
|
|
// Der Seite Platz verschaffen. `--honeybar-h` steht bereit, falls eine App
|
|
// ihre eigene 100vh-Rechnung anpassen muss (siehe README).
|
|
document.documentElement.style.setProperty("--honeybar-h", HOEHE);
|
|
if (pushMode === "body") {
|
|
// Nicht ausrechnen, sondern rechnen lassen: env() kennt nur der Browser,
|
|
// und erst beim Anzeigen. Der vorherige Wert bleibt als Summand stehen,
|
|
// damit eine App mit eigenem Innenabstand ihn nicht verliert.
|
|
const cs = getComputedStyle(document.body);
|
|
urspruenglichesPolster = cs.paddingTop || "0px";
|
|
document.body.style.paddingTop =
|
|
"calc(" + urspruenglichesPolster + " + var(--honeybar-h))";
|
|
}
|
|
|
|
render();
|
|
applySettings(settings);
|
|
verdrahten();
|
|
clock();
|
|
setInterval(clock, 20000);
|
|
|
|
systemZiele();
|
|
}
|
|
|
|
function verdrahten() {
|
|
if (verdrahtet) return;
|
|
verdrahtet = true;
|
|
|
|
// 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.
|
|
document.addEventListener("click", close);
|
|
root.addEventListener("click", (e) => {
|
|
const drin = paare.some(([m, b]) => e.composedPath().includes(m) || e.target === b);
|
|
if (!drin) close();
|
|
});
|
|
|
|
root.getElementById("hide").addEventListener("click", () => {
|
|
try {
|
|
localStorage.setItem(LS_HIDDEN, "1");
|
|
} catch {
|
|
/* dann eben nur für diesen Aufruf */
|
|
}
|
|
unmount();
|
|
mountNub();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Der eine Weg ins System: ein Link auf den Schreibtisch, keine Aktion hier.
|
|
*
|
|
* Das ist keine Bequemlichkeit, sondern die Grenze: Einstellungen
|
|
* **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.
|
|
*
|
|
* Hier stand deshalb lange ein zweiter Punkt „Einstellungen …", der auf
|
|
* dieselbe Seite fuehrte, nur mit geoeffnetem Feld. **Weg damit** (Marcs
|
|
* Einwand, 01.09.2026): Zwei Tueren in denselben Raum sind keine zwei Wege.
|
|
* Wer eine Einstellung aendern will, ist ohnehin auf dem Schreibtisch, und
|
|
* dort steht sie einen Klick oder ein [S] entfernt.
|
|
*
|
|
* Abmelden steht hier bewusst gar nicht: Es gehoert dorthin, wo man sich
|
|
* angemeldet hat — unten in den Einstellungen.
|
|
*/
|
|
function systemZiele() {
|
|
root.getElementById("mHome").href = new URL(homeUrl, location.href).href;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* ist der Weg zurück. Ganz verschwinden darf die HoneyBar nicht: wer sie einmal
|
|
* weggeklickt hat, käme sonst nur noch über die Entwicklerkonsole an sie heran.
|
|
*/
|
|
let nub = null;
|
|
|
|
function mountNub() {
|
|
if (nub) return;
|
|
nub = document.createElement("honey-bar-nub");
|
|
nub.style.cssText = [
|
|
"position:fixed", "left:0", "right:0", "height:3px",
|
|
"top:" + SAFE,
|
|
"z-index:2147483000", "cursor:pointer", "display:block",
|
|
].join(";");
|
|
nub.title = "HoneyBar einblenden";
|
|
const r = nub.attachShadow({ mode: "open" });
|
|
// Der Strich trug seine Farben fest verdrahtet und blieb deshalb bernstein,
|
|
// auch wenn in HoneyOS Grün eingestellt war. Er gehört zur HoneyBar, also
|
|
// trägt er dieselbe Röhre.
|
|
const t = ROEHREN[(settings && settings.phosphor) || "amber"] || ROEHREN.amber;
|
|
r.innerHTML = `
|
|
<style>
|
|
.strich {
|
|
height: 3px;
|
|
background: linear-gradient(90deg, ${t.dim}, ${t.p} 50%, ${t.dim});
|
|
opacity: .55;
|
|
transition: opacity .15s, height .15s;
|
|
}
|
|
:host(:hover) .strich { opacity: 1; height: 3px; }
|
|
</style>
|
|
<div class="strich"></div>`;
|
|
|
|
nub.addEventListener("click", () => {
|
|
forget();
|
|
nub.remove();
|
|
nub = null;
|
|
mount();
|
|
});
|
|
document.body.appendChild(nub);
|
|
}
|
|
|
|
function forget() {
|
|
try {
|
|
localStorage.removeItem(LS_HIDDEN);
|
|
} catch {
|
|
/* dann gilt es nur für diesen Aufruf */
|
|
}
|
|
}
|
|
|
|
function unmount() {
|
|
host.remove();
|
|
if (pushMode === "body" && urspruenglichesPolster !== null) {
|
|
document.body.style.paddingTop = urspruenglichesPolster;
|
|
urspruenglichesPolster = null;
|
|
}
|
|
document.documentElement.style.removeProperty("--honeybar-h");
|
|
}
|
|
|
|
async function start() {
|
|
let hidden = false;
|
|
try {
|
|
hidden = localStorage.getItem(LS_HIDDEN) === "1";
|
|
} catch {
|
|
/* kein Speicher, HoneyBar bleibt sichtbar */
|
|
}
|
|
// ?honeybar=1 holt sie zurück, falls der Strich einmal übersehen wird.
|
|
if (hidden && new URLSearchParams(location.search).get("honeybar") === "1") {
|
|
hidden = false;
|
|
forget();
|
|
}
|
|
if (hidden) {
|
|
if (document.body) mountNub();
|
|
else document.addEventListener("DOMContentLoaded", mountNub, { once: true });
|
|
return;
|
|
}
|
|
|
|
if (appsUrl) {
|
|
try {
|
|
// credentials: die Sitzung liegt auf der HoneyOS-Domain, und die ist
|
|
// dieselbe Site wie diese hier — sonst käme das Cookie nicht mit.
|
|
const res = await fetch(appsUrl, {
|
|
cache: "no-store",
|
|
credentials: "include",
|
|
headers: appsKey ? { "X-HoneyOS-Key": appsKey } : {},
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
if (Array.isArray(data.apps) && data.apps.length) {
|
|
apps = [{ id: "honeyos", name: "DESKTOP", url: homeUrl }].concat(data.apps);
|
|
}
|
|
settings = data.settings || null;
|
|
if (data.glyphs) glyphs = data.glyphs;
|
|
}
|
|
} catch {
|
|
/* nicht angemeldet, kein CORS, kein Netz → eingebaute Liste;
|
|
die HoneyBar bleibt ein Umschalter, nur ohne frische Namen */
|
|
}
|
|
}
|
|
|
|
if (document.body) mount();
|
|
else document.addEventListener("DOMContentLoaded", mount, { once: true });
|
|
|
|
wetter();
|
|
}
|
|
|
|
start();
|
|
})();
|