Files
BSO-Launcher/ui/app.js
T
MrMahoandClaude Opus 5.5 b69b9638ff Erste Version
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:26:25 +01:00

295 lines
13 KiB
JavaScript

"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();
})();