Der Dienst im Beispiel heisst jetzt einsetzer — er ist nicht die HoneyBar, er setzt sie ein. Zusammensetzungen wie Menueleiste, Taskleiste und Statusleiste bleiben, die meinen etwas anderes. Dazu in IDEAS notiert, dass HoneyBar und Menueleiste zwei getrennte Stilsaetze sind und schon einmal auseinandergelaufen ist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
483 lines
19 KiB
JavaScript
483 lines
19 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;
|
||
// 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 + ")";
|
||
|
||
/* Wettersymbole als Pixelraster, dieselben Namen wie in weather.go.
|
||
Sie stehen auch in os.js — bis die geteilte Symbolquelle (1.3) gebaut ist,
|
||
ist das die kleinere Sünde: Die HoneyBar soll eine einzelne Datei bleiben,
|
||
die eine fremde App mit einer Zeile einbindet. */
|
||
const WX = {
|
||
sonne: "............;.....##.....;..#..##..#..;...#....#...;....####....;..##....##..;..##....##..;....####....;...#....#...;..#..##..#..;.....##.....;............",
|
||
heiter: "............;...##.......;..####..#...;...##..###..;......#####.;...####...#.;..#....##.#.;.##........#;#..........#;.##########.;............;............",
|
||
wolke: "............;....####....;...#....#...;..#......##.;.##........#;##.........#;#..........#;#..........#;.##########.;............;............;............",
|
||
nebel: "............;....####....;..##....##..;.##.......#.;#..........#;.##########.;............;.#########..;............;..#########.;............;.#########..",
|
||
regen: "............;....####....;...#....#...;..#......##.;.##........#;##.........#;.##########.;............;..#..#..#...;.#..#..#....;..#..#..#...;.#..#..#....",
|
||
schnee: "............;....####....;...#....#...;..#......##.;.##........#;##.........#;.##########.;............;..#...#...#.;.#.#.#.#.#..;..#...#...#.;............",
|
||
gewitter: "............;....####....;...#....#...;..#......##.;.##........#;##.........#;.##########.;......##....;.....##.....;....####....;......##....;.....##.....",
|
||
};
|
||
|
||
function wxSvg(name) {
|
||
const grid = (WX[name] || WX.wolke).split(";");
|
||
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" 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(";");
|
||
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; }
|
||
.home:hover { border-color: var(--p-dim); }
|
||
.apps { display: flex; gap: 4px; flex: 1; overflow-x: auto; scrollbar-width: none; }
|
||
.apps::-webkit-scrollbar { display: none; }
|
||
.app { color: var(--p-mid); letter-spacing: .08em; }
|
||
.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; }
|
||
.user { color: var(--p); max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.user[hidden] { display: none; }
|
||
.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);
|
||
}
|
||
.menu[data-open="1"] { display: flex; }
|
||
.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); }
|
||
.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);
|
||
}
|
||
@media (max-width: 560px) { .clock { display: none; } }
|
||
</style>
|
||
|
||
<div class="bar">
|
||
<a class="home" id="home" title="HoneyOS">■ HONEY<b>OS</b></a>
|
||
<nav class="apps" id="apps"></nav>
|
||
<div class="right">
|
||
<span class="user" id="user" hidden></span>
|
||
<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" 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 ─────────────────────────────────────────────────────────────
|
||
* 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 = "";
|
||
|
||
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;
|
||
a.textContent = app.name;
|
||
if (app.id === current) a.setAttribute("aria-current", "page");
|
||
nav.appendChild(a);
|
||
|
||
const m = document.createElement("a");
|
||
m.href = app.url;
|
||
m.setAttribute("role", "menuitem");
|
||
m.textContent = (app.id === current ? "▸ " : " ") + app.name;
|
||
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 = wxSvg(w.glyph) + `<span class="grad">${Math.round(w.grad)}°</span>`;
|
||
el.title = `${w.ort}: ${w.text}, ${Math.round(w.grad)}°`;
|
||
el.hidden = false;
|
||
}
|
||
|
||
/**
|
||
* Zeigt an, wer angemeldet ist. Die HoneyBar steckt in der App selbst, der
|
||
* Aufruf geht also an denselben Ursprung — kein CORS, keine Freigabe nötig.
|
||
* Antwortet die App nicht wie erwartet, bleibt das Feld leer und niemand
|
||
* merkt etwas: die Anzeige ist eine Zugabe, keine Zusicherung.
|
||
*/
|
||
async function showUser() {
|
||
const me = apps.find((a) => a.id === current);
|
||
if (!me || !me.me) return;
|
||
try {
|
||
const headers = {};
|
||
if (me.tokenKey) {
|
||
const tok = localStorage.getItem(me.tokenKey);
|
||
if (tok) headers.Authorization = "Bearer " + tok;
|
||
}
|
||
const res = await fetch(url(me, me.me), { credentials: "same-origin", cache: "no-store", headers });
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
const u = (data && (data.user || data.profile)) || data || {};
|
||
const name = u.display_name || u.displayName || u.name || u.username || u.preferred_username;
|
||
if (!name) return;
|
||
const el = root.getElementById("user");
|
||
el.textContent = name;
|
||
el.hidden = false;
|
||
} catch {
|
||
/* nicht angemeldet, kein Endpunkt, kein Netz — alles kein Grund zu lärmen */
|
||
}
|
||
}
|
||
|
||
function url(app, path) {
|
||
if (!path) return app.url;
|
||
if (/^https?:\/\//i.test(path)) return path;
|
||
return app.url.replace(/\/+$/, "") + (path.startsWith("/") ? path : "/" + path);
|
||
}
|
||
|
||
function clock() {
|
||
root.getElementById("clock").textContent = new Date().toLocaleTimeString("de-DE", {
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
});
|
||
}
|
||
|
||
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);
|
||
clock();
|
||
setInterval(clock, 20000);
|
||
|
||
root.getElementById("home").href = homeUrl;
|
||
|
||
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));
|
||
});
|
||
// 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();
|
||
});
|
||
|
||
root.getElementById("hide").addEventListener("click", () => {
|
||
try {
|
||
localStorage.setItem(LS_HIDDEN, "1");
|
||
} catch {
|
||
/* dann eben nur für diesen Aufruf */
|
||
}
|
||
unmount();
|
||
mountNub();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 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" });
|
||
r.innerHTML = `
|
||
<style>
|
||
.strich {
|
||
height: 3px;
|
||
background: linear-gradient(90deg, #6b4718, #ffb641 50%, #6b4718);
|
||
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();
|
||
showUser();
|
||
});
|
||
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;
|
||
}
|
||
} 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 });
|
||
|
||
showUser();
|
||
wetter();
|
||
}
|
||
|
||
start();
|
||
})();
|