+163
@@ -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); } }
|
||||
@@ -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 ? "" : ""; });
|
||||
|
||||
// ---------------------------------------------------------------- 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();
|
||||
})();
|
||||
@@ -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"></button>
|
||||
<button type="button" data-window="maximize" id="maximizeButton" title="Maximieren" aria-label="Maximieren"></button>
|
||||
<button type="button" data-window="close" class="close" title="Schliessen" aria-label="Schliessen"></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"></span>Spielen</button>
|
||||
<button type="button" data-page="news"><span class="nav-icon"></span>Neuigkeiten</button>
|
||||
<button type="button" data-page="settings"><span class="nav-icon"></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"></button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<section class="content">
|
||||
<div id="page" class="page"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="toasts" class="toasts"></div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
+32
@@ -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 |
Reference in New Issue
Block a user