Erste Version

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 17:26:25 +01:00
co-authored by Claude Opus 5.5
commit b69b9638ff
20 changed files with 1863 additions and 0 deletions
+163
View File
@@ -0,0 +1,163 @@
:root {
--bg: #0b0d12;
--panel: #151922;
--panel-2: #1c212c;
--sidebar: #080a0e;
--border: #262c38;
--text: #eaf0f7;
--muted: #8b95a5;
--accent: #2f9bff;
--accent-2: #5ee4ff;
--online: #3dd68c;
--pending: #f2c230;
--danger: #e5484d;
--radius: 12px;
--titlebar: 36px;
--icons: "Segoe Fluent Icons", "Segoe MDL2 Assets";
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body {
background: var(--bg);
color: var(--text);
font: 14px/1.5 "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
overflow: hidden;
user-select: none;
}
input { user-select: text; }
.hidden { display: none !important; }
.muted { color: var(--muted); }
.small { font-size: 12.5px; }
.center { text-align: center; }
.error { color: #ff8a8d; margin: 0; }
h1, h2, h3 { margin: 0; letter-spacing: -0.01em; }
h1 { font-size: 26px; }
h2 { font-size: 17px; }
code, .mono { font-family: "Cascadia Mono", Consolas, monospace; font-size: 12.5px; }
/* ---------------------------------------------------------------- Titelleiste */
.titlebar { position: fixed; inset: 0 0 auto 0; height: var(--titlebar); display: flex; z-index: 50;
background: var(--sidebar); border-bottom: 1px solid #151922; }
.titlebar-drag { flex: 1; display: flex; align-items: center; gap: 8px; padding: 0 12px; color: var(--muted); font-size: 12px; font-weight: 600; }
.titlebar-drag img { width: 18px; height: 18px; }
.window-buttons { display: flex; }
.window-buttons button { width: 46px; border: 0; background: transparent; color: #c4ccd8; font: 10px var(--icons); cursor: default; }
.window-buttons button:hover { background: #1c212c; }
.window-buttons .close:hover { background: #c42b1c; color: #fff; }
/* ---------------------------------------------------------------- Knoepfe & Felder */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; border: 1px solid var(--border);
background: var(--panel-2); color: var(--text); font: inherit; font-weight: 700; padding: 9px 16px; border-radius: 10px;
cursor: pointer; transition: background .12s, border-color .12s, transform .06s; white-space: nowrap; }
.btn:hover { background: #252b38; border-color: #36404f; }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: .5; cursor: default; transform: none; }
.btn.primary { background: linear-gradient(180deg, #45adff, #1f7fe5); border-color: #5cb8ff; color: #fff;
box-shadow: 0 8px 24px rgba(47, 155, 255, .25); }
.btn.primary:hover { background: linear-gradient(180deg, #5cb8ff, #2f8cf0); }
.btn.danger { color: #ff9a9d; border-color: #5a2a2e; background: #2a1517; }
.btn.small { padding: 6px 12px; font-size: 12.5px; }
.btn.wide { width: 100%; }
.btn.play { font-size: 22px; letter-spacing: .08em; padding: 16px 56px; border-radius: 14px; }
input[type=text], input[type=password], input[type=email], input:not([type]) { width: 100%; background: var(--bg); border: 1px solid var(--border);
color: var(--text); font: inherit; padding: 10px 12px; border-radius: 10px; outline: none; }
input:focus { border-color: var(--accent); }
label { display: grid; gap: 6px; font-size: 12.5px; font-weight: 600; color: #b4bdca; }
/* Schalter */
.switch { display: flex; align-items: center; justify-content: space-between; gap: 16px; font-size: 13.5px; font-weight: 600; color: var(--text); cursor: pointer; }
.switch input { appearance: none; width: 40px; height: 22px; border-radius: 11px; background: #2a3140; position: relative; cursor: pointer; flex: none;
transition: background .15s; }
.switch input::after { content: ""; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #c4ccd8;
transition: transform .15s; }
.switch input:checked { background: var(--accent); }
.switch input:checked::after { transform: translateX(18px); background: #fff; }
.switch small { display: block; font-weight: 400; color: var(--muted); }
/* ---------------------------------------------------------------- Anmeldung */
.auth { position: fixed; inset: var(--titlebar) 0 0 0; display: grid; place-items: center; padding: 16px;
background: radial-gradient(900px 500px at 50% -10%, rgba(47, 155, 255, .2), transparent 60%), var(--bg); }
.auth-card { width: min(400px, 100%); background: var(--panel); border: 1px solid var(--border); border-radius: 18px; padding: 28px;
display: grid; gap: 14px; box-shadow: 0 30px 80px rgba(0, 0, 0, .45); }
.tabs { display: grid; grid-template-columns: 1fr 1fr; padding: 4px; border-radius: 12px; background: var(--bg); border: 1px solid var(--border); }
.tab { border: 0; background: transparent; color: var(--muted); font: inherit; font-weight: 700; padding: 8px; border-radius: 9px; cursor: pointer; }
.tab.on { background: var(--panel-2); color: var(--text); }
.brand { display: flex; align-items: center; gap: 12px; }
.brand img { width: 40px; height: 40px; }
.brand.big img { width: 56px; height: 56px; }
.brand-title { font-size: 18px; font-weight: 800; letter-spacing: .02em; }
.brand.big .brand-title { font-size: 24px; }
.brand-sub { color: var(--muted); font-size: 12.5px; }
.beta-pill { font-size: 10px; font-weight: 800; letter-spacing: .08em; vertical-align: middle; padding: 2px 6px; border-radius: 6px;
color: var(--accent-2); background: rgba(94, 228, 255, .1); border: 1px solid rgba(94, 228, 255, .3); }
/* ---------------------------------------------------------------- Aufbau */
.layout { position: fixed; inset: var(--titlebar) 0 0 0; display: grid; grid-template-columns: 240px 1fr; }
.sidebar { background: var(--sidebar); border-right: 1px solid #151922; display: flex; flex-direction: column; padding: 20px 14px; gap: 22px; }
nav { display: grid; gap: 4px; }
nav button { display: flex; align-items: center; gap: 12px; border: 0; background: transparent; color: var(--muted); font: inherit; font-weight: 600;
padding: 10px 12px; border-radius: 10px; cursor: pointer; text-align: left; }
nav button:hover { background: #12161e; color: var(--text); }
nav button.on { background: var(--panel-2); color: var(--text); }
.nav-icon { font: 16px var(--icons); width: 18px; }
.sidebar-foot { margin-top: auto; }
.me-card { display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 12px; background: var(--panel); border: 1px solid var(--border); }
.avatar { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; font-weight: 800; color: #fff;
background: linear-gradient(135deg, var(--accent-2), #1f6fe5); flex: none; }
.me-text { display: grid; min-width: 0; flex: 1; }
.me-text b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.me-text small { color: var(--muted); font-size: 11.5px; }
.icon-btn { border: 0; background: transparent; color: var(--muted); font: 15px var(--icons); padding: 6px; border-radius: 8px; cursor: pointer; }
.icon-btn:hover { background: var(--panel-2); color: var(--text); }
.content { overflow-y: auto; }
.page { max-width: 980px; margin: 0 auto; padding: 28px 32px 40px; display: grid; gap: 20px; }
/* ---------------------------------------------------------------- Spielen */
.hero { position: relative; overflow: hidden; border-radius: 18px; border: 1px solid var(--border); padding: 36px;
background: radial-gradient(700px 320px at 85% 0%, rgba(94, 228, 255, .16), transparent 60%),
radial-gradient(500px 300px at 0% 100%, rgba(31, 111, 229, .22), transparent 60%), var(--panel);
display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 24px; }
.hero h1 { font-size: 32px; }
.hero p { margin: 6px 0 0; color: var(--muted); }
.hero img { width: 120px; height: 120px; filter: drop-shadow(0 12px 30px rgba(47, 155, 255, .35)); }
.hero-actions { grid-column: 1 / -1; display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.banner { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 18px; border-radius: 14px;
background: rgba(47, 155, 255, .1); border: 1px solid rgba(47, 155, 255, .35); }
.banner.warn { background: rgba(242, 194, 48, .08); border-color: rgba(242, 194, 48, .35); }
.progress { display: grid; gap: 8px; }
.bar { height: 8px; border-radius: 4px; background: #232a36; overflow: hidden; }
.bar > div { height: 100%; width: 0; background: linear-gradient(90deg, #1f6fe5, var(--accent-2)); transition: width .15s; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; }
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 14px; padding: 18px; display: grid; gap: 10px; align-content: start; }
.card h3 { font-size: 13px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); display: flex; align-items: center; gap: 8px; }
.card .value { font-size: 16px; font-weight: 700; word-break: break-all; }
.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted); display: inline-block; }
.dot.ok { background: var(--online); box-shadow: 0 0 10px rgba(61, 214, 140, .6); }
.dot.warn { background: var(--pending); }
.dot.bad { background: var(--danger); }
.row { display: flex; gap: 8px; align-items: center; }
/* ---------------------------------------------------------------- Neuigkeiten */
.release { background: var(--panel); border: 1px solid var(--border); border-radius: 14px; padding: 18px; display: grid; gap: 6px; }
.release header { display: flex; align-items: baseline; gap: 10px; }
.tag { font-size: 11px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; padding: 2px 8px; border-radius: 6px;
background: var(--panel-2); color: var(--accent-2); border: 1px solid var(--border); }
.release pre { margin: 0; white-space: pre-wrap; font: inherit; color: #c4ccd8; }
/* ---------------------------------------------------------------- Einstellungen */
.settings { display: grid; gap: 14px; }
.settings .card { gap: 16px; }
/* ---------------------------------------------------------------- Meldungen */
.toasts { position: fixed; right: 18px; bottom: 18px; display: grid; gap: 8px; z-index: 100; }
.toast { background: var(--panel-2); border: 1px solid var(--border); border-left: 3px solid var(--accent); padding: 10px 14px; border-radius: 10px;
max-width: 380px; box-shadow: 0 12px 30px rgba(0, 0, 0, .4); animation: in .18s ease-out; }
.toast.error { border-left-color: var(--danger); }
@keyframes in { from { opacity: 0; transform: translateY(8px); } }
+294
View File
@@ -0,0 +1,294 @@
"use strict";
// BSO Launcher - Oberflaeche. Spricht ueber window.chrome.webview mit dem Launcher (C#), nie direkt mit dem Netz.
// ---------------------------------------------------------------- Verbindung zum Launcher
let nextId = 1;
const pending = new Map();
const listeners = new Map();
function call(cmd, args) {
return new Promise((resolve, reject) => {
const id = nextId++;
pending.set(id, { resolve, reject });
window.chrome.webview.postMessage({ id, cmd, args: args || {} });
});
}
window.chrome.webview.addEventListener("message", (e) => {
const msg = e.data;
if (msg.event) { (listeners.get(msg.event) || []).forEach((f) => f(msg)); return; }
const p = pending.get(msg.id);
if (!p) return;
pending.delete(msg.id);
msg.ok ? p.resolve(msg.result) : p.reject(new Error(msg.error));
});
function on(event, f) {
if (!listeners.has(event)) listeners.set(event, []);
listeners.get(event).push(f);
}
// ---------------------------------------------------------------- Hilfen
const $ = (id) => document.getElementById(id);
function h(tag, attrs, ...children) {
const el = document.createElement(tag);
for (const [k, v] of Object.entries(attrs || {})) {
if (v == null || v === false) continue;
if (k.startsWith("on")) el.addEventListener(k.slice(2), v);
else if (k === "class") el.className = v;
else el.setAttribute(k, v === true ? "" : v);
}
for (const c of children.flat()) if (c != null && c !== false) el.append(c.nodeType ? c : String(c));
return el;
}
function toast(text, error) {
const t = h("div", { class: "toast" + (error ? " error" : "") }, text);
$("toasts").append(t);
setTimeout(() => t.remove(), error ? 7000 : 4000);
}
/** Knopf waehrend einer Aktion sperren; Fehler als Meldung. */
async function busy(button, action) {
button.disabled = true;
try { return await action(); }
catch (e) { toast(e.message, true); }
finally { button.disabled = false; }
}
const date = (iso) => new Date(iso).toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" });
// ---------------------------------------------------------------- Fenster
document.querySelectorAll("[data-window]").forEach((b) => b.addEventListener("click", () => call("window", { action: b.dataset.window })));
$("titlebarDrag").addEventListener("mousedown", (e) => { if (e.button === 0 && e.detail === 1) call("window", { action: "drag" }); });
$("titlebarDrag").addEventListener("dblclick", () => call("window", { action: "maximize" }));
on("window", (m) => { $("maximizeButton").innerHTML = m.maximized ? "&#xE923;" : "&#xE922;"; });
// ---------------------------------------------------------------- Anmeldung
let registerMode = false;
function setAuthMode(register) {
registerMode = register;
$("tabLogin").classList.toggle("on", !register);
$("tabRegister").classList.toggle("on", register);
$("emailRow").classList.toggle("hidden", !register);
$("password2Row").classList.toggle("hidden", !register);
$("authButton").textContent = register ? "Konto erstellen" : "Anmelden";
$("password").autocomplete = register ? "new-password" : "current-password";
$("authError").classList.add("hidden");
}
$("tabLogin").addEventListener("click", () => setAuthMode(false));
$("tabRegister").addEventListener("click", () => setAuthMode(true));
$("authForm").addEventListener("submit", async (e) => {
e.preventDefault();
const error = $("authError");
error.classList.add("hidden");
if (registerMode && $("password").value !== $("password2").value) {
error.textContent = "Die Passwoerter sind nicht gleich";
error.classList.remove("hidden");
return;
}
$("authButton").disabled = true;
try {
const r = await call(registerMode ? "register" : "login",
{ username: $("username").value, password: $("password").value, email: $("email").value });
$("password").value = $("password2").value = "";
showApp(r.username);
if (registerMode) toast("Willkommen bei BSO, " + r.username + "!");
} catch (err) {
error.textContent = err.message;
error.classList.remove("hidden");
} finally {
$("authButton").disabled = false;
}
});
$("logout").addEventListener("click", async () => {
await call("logout");
showAuth();
});
function showAuth() {
$("app").classList.add("hidden");
$("auth").classList.remove("hidden");
setAuthMode(false);
$("username").focus();
}
function showApp(username) {
$("auth").classList.add("hidden");
$("app").classList.remove("hidden");
$("meName").textContent = username;
$("meAvatar").textContent = (username || "?").slice(0, 1).toUpperCase();
showPage("home");
}
// ---------------------------------------------------------------- Seiten
let page = "home";
document.querySelectorAll("#nav [data-page]").forEach((b) => b.addEventListener("click", () => showPage(b.dataset.page)));
function showPage(name) {
page = name;
document.querySelectorAll("#nav [data-page]").forEach((b) => b.classList.toggle("on", b.dataset.page === name));
const root = $("page");
root.replaceChildren(h("p", { class: "muted" }, "Laden …"));
({ home: renderHome, news: renderNews, settings: renderSettings })[name](root);
}
// Fortschritt (Downloads)
let progressBox = null;
on("progress", (m) => {
if (!progressBox) return;
progressBox.classList.remove("hidden");
progressBox.querySelector(".bar > div").style.width = Math.round(m.value * 100) + "%";
progressBox.querySelector("span").textContent = m.text + " " + Math.round(m.value * 100) + " %";
});
async function renderHome(root) {
let s;
try { s = await call("status"); }
catch (e) { root.replaceChildren(h("p", { class: "error" }, e.message)); return; }
if (page !== "home") return;
progressBox = h("div", { class: "progress hidden" }, h("span", { class: "small muted" }), h("div", { class: "bar" }, h("div")));
const play = h("button", { class: "btn primary play", type: "button" }, s.game.running ? "LAEUFT" : "SPIELEN");
play.disabled = s.game.running;
play.addEventListener("click", () => busy(play, async () => {
await call("launch");
toast("Bus-Simulator 2012 startet …");
}));
const ready = s.game.found && s.dotnet && (s.mod.installed || s.mod.latest);
const line = s.mod.installed ? `BSO ${s.mod.installed} installiert · Server ${s.settings.server}` : "BSO wird beim ersten Start installiert";
const children = [
h("div", { class: "hero" },
h("div", {}, h("h1", {}, "Bus Simulator Online"), h("p", {}, "Multiplayer fuer Bus-Simulator 2012")),
h("img", { src: "logo.svg", alt: "" }),
h("div", { class: "hero-actions" }, play, h("span", { class: "muted small" }, ready ? line : "Vor dem Start fehlt noch etwas - siehe unten"))),
progressBox
];
if (s.launcherUpdate) {
const b = h("button", { class: "btn primary small", type: "button" }, "Jetzt aktualisieren");
b.addEventListener("click", () => busy(b, async () => { await call("installLauncherUpdate"); toast("Launcher startet neu …"); }));
children.push(h("div", { class: "banner" }, h("span", {}, h("b", {}, "Launcher " + s.launcherUpdate), " ist da."), b));
}
if (!s.masterReachable)
children.push(h("div", { class: "banner warn" }, h("span", {}, "BSO-Server nicht erreichbar - Updates werden spaeter geprueft.")));
children.push(h("div", { class: "cards" }, gameCard(s), modCard(s), dotnetCard(s), serverCard(s)));
root.replaceChildren(...children);
}
function card(title, state, value, ...extra) {
return h("div", { class: "card" }, h("h3", {}, h("span", { class: "dot " + state }), title), h("div", { class: "value" }, value), ...extra);
}
function gameCard(s) {
const choose = h("button", { class: "btn small", type: "button" }, s.game.found ? "Anderen Ordner waehlen" : "Ordner waehlen");
choose.addEventListener("click", () => busy(choose, async () => { if ((await call("chooseGameFolder")).changed) showPage("home"); }));
return card("Bus-Simulator 2012", s.game.found ? "ok" : "bad", s.game.found ? "Gefunden" : "Nicht gefunden",
s.game.found ? h("span", { class: "mono muted" }, s.game.folder) : h("span", { class: "muted small" }, "Ueber Steam installiert? Sonst den Ordner von Hand waehlen."),
h("div", { class: "row" }, choose));
}
function modCard(s) {
const m = s.mod;
const state = m.installed && !m.updateAvailable ? "ok" : m.latest ? "warn" : "bad";
const value = m.installed ? `Version ${m.installed}` : "Nicht installiert";
const info = m.updateAvailable ? `Neu: ${m.latest}` : m.latest ? "Aktuell" : "Keine Version auf dem Server";
const extra = [h("span", { class: "muted small" }, info)];
if (m.updateAvailable && s.game.found) {
const b = h("button", { class: "btn small", type: "button" }, m.installed ? "Aktualisieren" : "Installieren");
b.addEventListener("click", () => busy(b, async () => {
const r = await call("installMod");
toast("BSO " + r.version + " installiert");
showPage("home");
}));
extra.push(h("div", { class: "row" }, b));
}
return card("BSO-Mod", state, value, ...extra);
}
function dotnetCard(s) {
if (s.dotnet) return card(".NET 10 Runtime", "ok", "Installiert", h("span", { class: "muted small" }, "Die Mod laeuft mit .NET 10 (x64)."));
const b = h("button", { class: "btn small", type: "button" }, "Herunterladen");
b.addEventListener("click", () => call("openDotnet"));
return card(".NET 10 Runtime", "bad", "Fehlt",
h("span", { class: "muted small" }, "BSO braucht die .NET 10 Runtime (x64) - \".NET Runtime\" fuer Windows x64 installieren."),
h("div", { class: "row" }, b));
}
function serverCard(s) {
const input = h("input", { value: s.settings.server, placeholder: "host:7777", spellcheck: "false" });
const auto = h("input", { type: "checkbox" });
auto.checked = s.settings.autoConnect;
const save = () => call("setSettings", { server: input.value.trim(), autoConnect: auto.checked }).then(() => toast("Gespeichert"));
input.addEventListener("change", save);
auto.addEventListener("change", save);
return card("Server", "ok", "Mitspielen",
h("label", {}, h("span", {}, "Adresse"), input),
h("label", { class: "switch" }, h("span", {}, "Beim Start verbinden", h("small", {}, "sonst im Spiel mit F8")), auto));
}
async function renderNews(root) {
let list;
try { list = await call("releases"); }
catch (e) { root.replaceChildren(h("h1", {}, "Neuigkeiten"), h("p", { class: "error" }, e.message)); return; }
if (page !== "news") return;
const names = { mod: "Mod", launcher: "Launcher", server: "Server" };
root.replaceChildren(h("h1", {}, "Neuigkeiten"),
...(list.length ? list.map((r) => h("article", { class: "release" },
h("header", {}, h("span", { class: "tag" }, names[r.product] || r.product), h("h2", {}, r.version), h("span", { class: "muted small" }, date(r.date))),
h("pre", {}, r.changelog || "Keine Beschreibung"))) : [h("p", { class: "muted" }, "Noch keine Versionen veroeffentlicht.")]));
}
async function renderSettings(root) {
const [init, s] = await Promise.all([call("init"), call("status").catch(() => null)]);
if (page !== "settings") return;
const toggle = (label, hint, key, value) => {
const box = h("input", { type: "checkbox" });
box.checked = value;
box.addEventListener("change", () => call("setSettings", { [key]: box.checked }).then(() => toast("Gespeichert")));
return h("label", { class: "switch" }, h("span", {}, label, h("small", {}, hint)), box);
};
const folder = h("button", { class: "btn small", type: "button" }, "Spielordner waehlen");
folder.addEventListener("click", () => busy(folder, async () => { if ((await call("chooseGameFolder")).changed) { toast("Spielordner gespeichert"); showPage("settings"); } }));
const log = h("button", { class: "btn small", type: "button" }, "Protokoll-Ordner oeffnen");
log.addEventListener("click", () => call("openLog"));
const uninstall = h("button", { class: "btn small danger", type: "button" }, "BSO deinstallieren");
uninstall.addEventListener("click", () => busy(uninstall, () => call("uninstall")));
root.replaceChildren(h("h1", {}, "Einstellungen"), h("div", { class: "settings" },
h("div", { class: "card" },
toggle("BSO automatisch aktualisieren", "Vor jedem Start die neueste Mod-Version installieren", "autoUpdateMod", s?.settings.autoUpdateMod ?? true),
toggle("Launcher offen lassen", "Sonst schliesst er sich nach dem Spielstart", "keepOpen", s?.settings.keepOpen ?? false)),
h("div", { class: "card" },
h("h3", {}, "Spiel"),
h("span", { class: "mono muted" }, s?.game.folder || "Nicht gefunden"),
h("div", { class: "row" }, folder)),
h("div", { class: "card" },
h("h3", {}, "Launcher"),
h("span", { class: "muted small" }, `Version ${init.version} · Server ${init.master}`),
h("div", { class: "row" }, log, uninstall))));
}
// ---------------------------------------------------------------- Start
(async () => {
const init = await call("init");
$("meVersion").textContent = "Launcher " + init.version;
if (!init.loggedIn) { showAuth(); return; }
const me = await call("me");
if (me.loggedIn) showApp(me.username);
else showAuth();
})();
+86
View File
@@ -0,0 +1,86 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BSO Launcher</title>
<link rel="stylesheet" href="app.css">
<script src="app.js" defer></script>
</head>
<body>
<!-- Eigene Titelleiste (das Fenster hat keine von Windows) -->
<header class="titlebar">
<div class="titlebar-drag" id="titlebarDrag">
<img src="logo.png" alt="">
<span>BSO Launcher</span>
</div>
<div class="window-buttons">
<button type="button" data-window="minimize" title="Minimieren" aria-label="Minimieren">&#xE921;</button>
<button type="button" data-window="maximize" id="maximizeButton" title="Maximieren" aria-label="Maximieren">&#xE922;</button>
<button type="button" data-window="close" class="close" title="Schliessen" aria-label="Schliessen">&#xE8BB;</button>
</div>
</header>
<!-- Anmeldung / Registrierung -->
<main id="auth" class="auth hidden">
<form id="authForm" class="auth-card" autocomplete="on">
<div class="brand big">
<img src="logo.svg" alt="">
<div>
<div class="brand-title">BSO <span class="beta-pill">BETA</span></div>
<div class="brand-sub">Bus Simulator Online</div>
</div>
</div>
<div class="tabs">
<button type="button" class="tab on" id="tabLogin">Anmelden</button>
<button type="button" class="tab" id="tabRegister">Konto erstellen</button>
</div>
<label><span>Benutzername</span>
<input id="username" autocomplete="username" maxlength="32" required>
</label>
<label id="emailRow" class="hidden"><span>E-Mail (optional)</span>
<input id="email" type="email" autocomplete="email" maxlength="190">
</label>
<label><span>Passwort</span>
<input id="password" type="password" autocomplete="current-password" maxlength="200" required>
</label>
<label id="password2Row" class="hidden"><span>Passwort wiederholen</span>
<input id="password2" type="password" autocomplete="new-password" maxlength="200">
</label>
<p id="authError" class="error hidden"></p>
<button class="btn primary wide" id="authButton" type="submit">Anmelden</button>
<p class="muted small center">Dein Benutzername ist auch dein Name im Spiel. Passwoerter liegen nur verschluesselt auf dem BSO-Server.</p>
</form>
</main>
<!-- Launcher -->
<div id="app" class="layout hidden">
<aside class="sidebar">
<div class="brand">
<img src="logo.svg" alt="">
<div>
<div class="brand-title">BSO <span class="beta-pill">BETA</span></div>
<div class="brand-sub">Launcher</div>
</div>
</div>
<nav id="nav">
<button type="button" data-page="home" class="on"><span class="nav-icon">&#xE768;</span>Spielen</button>
<button type="button" data-page="news"><span class="nav-icon">&#xE789;</span>Neuigkeiten</button>
<button type="button" data-page="settings"><span class="nav-icon">&#xE713;</span>Einstellungen</button>
</nav>
<div class="sidebar-foot">
<div class="me-card">
<span class="avatar" id="meAvatar">?</span>
<span class="me-text"><b id="meName"></b><small id="meVersion"></small></span>
<button class="icon-btn" id="logout" type="button" title="Abmelden" aria-label="Abmelden">&#xF3B1;</button>
</div>
</div>
</aside>
<section class="content">
<div id="page" class="page"></div>
</section>
</div>
<div id="toasts" class="toasts"></div>
</body>
</html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+32
View File
@@ -0,0 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#5ee4ff"/>
<stop offset="1" stop-color="#1f6fe5"/>
</linearGradient>
</defs>
<rect x="2" y="2" width="60" height="60" rx="14" fill="#0d1016" stroke="url(#g)" stroke-width="3"/>
<!-- Funkwellen: online -->
<path d="M26.5 15.5a8 8 0 0 1 11 0" fill="none" stroke="url(#g)" stroke-width="2.6" stroke-linecap="round"/>
<path d="M23 12a13 13 0 0 1 18 0" fill="none" stroke="url(#g)" stroke-width="2.6" stroke-linecap="round" opacity=".55"/>
<circle cx="32" cy="18.6" r="1.9" fill="#5ee4ff"/>
<!-- Stadtbus, Front rechts -->
<path d="M6.5 44V27.2c0-1.8 1.4-3.2 3.2-3.2h41.4c1.4 0 2.6.9 3.1 2.2l3 7.6c.2.5.3 1 .3 1.6V44z" fill="url(#g)"/>
<!-- Fenster, Tuer, Frontscheibe -->
<rect x="9.5" y="27" width="7.2" height="6.4" rx="1.2" fill="#0d1016"/>
<rect x="18.2" y="27" width="7.2" height="6.4" rx="1.2" fill="#0d1016"/>
<rect x="26.9" y="27" width="7.2" height="6.4" rx="1.2" fill="#0d1016"/>
<rect x="35.6" y="27" width="7.2" height="6.4" rx="1.2" fill="#0d1016"/>
<rect x="44.6" y="27" width="5.4" height="14.5" rx="1" fill="#0d1016"/>
<path d="M47.3 27v14.5" stroke="url(#g)" stroke-width=".9"/>
<path d="M51.6 27h.9c.6 0 1.1.4 1.3.9l2.5 6.4h-4.7z" fill="#0d1016"/>
<!-- Zierstreifen, Scheinwerfer -->
<path d="M6.5 37h37.6" stroke="#0d1016" stroke-width="1.6" opacity=".5"/>
<rect x="54.4" y="38.4" width="2.8" height="1.9" rx=".8" fill="#fff4c9"/>
<!-- Raeder -->
<circle cx="16" cy="44" r="5.6" fill="#0d1016"/>
<circle cx="39" cy="44" r="5.6" fill="#0d1016"/>
<circle cx="16" cy="44" r="3.2" fill="none" stroke="#5ee4ff" stroke-width="2.2"/>
<circle cx="39" cy="44" r="3.2" fill="none" stroke="#5ee4ff" stroke-width="2.2"/>
<path d="M9 52.5h46" stroke="#232a36" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB