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