Files
BSO-Launcher/ui/app.js
T
MrMahoandClaude Opus 5.5 c7a30e7fa3 Auto-Update wie beim MSCO Launcher
Beim Start installiert sich ein neuer Launcher sofort selbst (Fortschrittsfenster, Neustart), neue
BSO-Versionen kommen nach dem Anmelden - beides abschaltbar. Einstellungen: Nach Updates suchen /
Installieren und neu starten. Updates wieder nur von der Website; dotnet publish laedt hoch (release.ps1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:19:48 +01:00

529 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
// BSO Launcher - Oberflaeche. Spricht ueber window.chrome.webview mit dem Launcher (C#), nie direkt mit dem Netz.
// Texte auf Deutsch im Code, t() uebersetzt (i18n.js).
// ---------------------------------------------------------------- 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 el = h("div", { class: "toast" + (error ? " error" : "") }, text);
$("toasts").append(el);
setTimeout(() => el.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(lang === "en" ? "en-GB" : "de-DE", { day: "2-digit", month: "2-digit", year: "numeric" });
// ---------------------------------------------------------------- Sprache
/** Sprache wechseln: im Launcher speichern (auch fuer seine eigenen Meldungen), alles neu beschriften. */
async function setLang(l) {
lang = l;
await call("setSettings", { language: l });
translateStatic();
renderLangSwitch(setLang);
setAuthMode(registerMode);
if (!$("app").classList.contains("hidden")) showPage(page);
}
// ---------------------------------------------------------------- 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 ? t("Konto erstellen") : t("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 = t("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(t("Willkommen bei BSO, {0}!", 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");
autoUpdateMod();
}
// ---------------------------------------------------------------- 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" }, t("Laden …")));
({ home: renderHome, friends: renderFriends, news: renderNews, settings: renderSettings })[name](root);
}
// Anmeldung abgelaufen (z.B. beim Holen des Spiel-Tickets)
on("loggedOut", () => showAuth());
// Fortschritt (Downloads) - der Text kommt schon uebersetzt vom Launcher
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 ? t("LAEUFT") : t("SPIELEN"));
play.disabled = s.game.running;
play.addEventListener("click", () => busy(play, async () => {
await call("launch");
toast(t("Bus-Simulator 2012 startet …"));
}));
const ready = s.game.found && s.dotnet && (s.mod.installed || s.mod.latest);
const line = s.mod.installed ? t("BSO {0} installiert · zusammen spielen unter Freunde & Lobby", s.mod.installed) : t("BSO wird beim ersten Start installiert");
const children = [
h("div", { class: "hero" },
h("div", {}, h("h1", {}, "Bus Simulator Online"), h("p", {}, t("Multiplayer fuer Bus-Simulator 2012"))),
h("img", { src: "logo.svg", alt: "" }),
h("div", { class: "hero-actions" }, play, h("span", { class: "muted small" }, ready ? line : t("Vor dem Start fehlt noch etwas - siehe unten")))),
progressBox
];
if (s.launcherUpdate) {
const b = h("button", { class: "btn primary small", type: "button" }, t("Jetzt aktualisieren"));
b.addEventListener("click", () => busy(b, async () => { await call("installLauncherUpdate"); toast(t("Launcher startet neu …")); }));
children.push(h("div", { class: "banner" }, h("span", {}, h("b", {}, t("Launcher {0}", s.launcherUpdate)), t(" ist da.")), b));
}
if (!s.masterReachable)
children.push(h("div", { class: "banner warn" }, h("span", {}, t("BSO-Server nicht erreichbar - Updates werden spaeter geprueft."))));
children.push(h("div", { class: "cards" }, gameCard(s), modCard(s), dotnetCard(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 ? t("Anderen Ordner waehlen") : t("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 ? t("Gefunden") : t("Nicht gefunden"),
s.game.found ? h("span", { class: "mono muted" }, s.game.folder) : h("span", { class: "muted small" }, t("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 ? t("Version {0}", m.installed) : t("Nicht installiert");
const info = m.updateAvailable ? t("Neu: {0}", m.latest) : m.latest ? t("Aktuell") : t("Keine Version auf dem Server");
const extra = [h("span", { class: "muted small" }, info)];
// Immer da: holt die neueste Mod von der Website (bei SPIELEN passiert das sonst automatisch, wenn eingeschaltet)
if (m.latest && s.game.found) {
const label = !m.installed ? t("Mod herunterladen") : m.updateAvailable ? t("Mod aktualisieren") : t("Neu herunterladen");
const b = h("button", { class: "btn small" + (!m.installed || m.updateAvailable ? " primary" : ""), type: "button" }, label);
b.addEventListener("click", () => busy(b, async () => {
const r = await call("installMod");
toast(t("BSO {0} installiert", r.version));
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", t("Installiert"), h("span", { class: "muted small" }, t("Die Mod laeuft mit .NET 10 (x64).")));
const b = h("button", { class: "btn small", type: "button" }, t("Herunterladen"));
b.addEventListener("click", () => call("openDotnet"));
return card(".NET 10 Runtime", "bad", t("Fehlt"),
h("span", { class: "muted small" }, t("BSO braucht die .NET 10 Runtime (x64) - \".NET Runtime\" fuer Windows x64 installieren.")),
h("div", { class: "row" }, b));
}
// ---------------------------------------------------------------- Freunde & Lobby
// Die Lobby liegt auf der Website, ihr Server laeuft im BSO-Relay - keiner hostet, keiner braucht eine Portfreigabe.
let social = { friends: { friends: [], incoming: [], outgoing: [] }, lobby: { lobby: null, invites: [] } };
const seenNotices = new Set();
async function loadSocial() {
const [friends, lobby] = await Promise.all([call("friends"), call("lobby")]);
social = { friends, lobby };
const n = friends.incoming.length + lobby.invites.length;
$("friendsBadge").textContent = n;
$("friendsBadge").classList.toggle("hidden", n === 0);
// Neue Einladungen/Anfragen melden (auch auf anderen Seiten)
for (const i of lobby.invites) notice("l" + i.lobbyId, t("{0} lädt dich in die Lobby ein", i.owner));
for (const r of friends.incoming) notice("f" + r.id, t("{0} möchte dein Freund sein", r.username));
}
function notice(key, text) {
if (seenNotices.has(key)) return;
seenNotices.add(key);
if (page !== "friends") toast(text);
}
/** Alle 3 s auf der Seite Freunde, sonst alle 15 s (Badge, Meldungen) - meldet einen auch als "online". */
let socialTick = 0;
setInterval(async () => {
if ($("app").classList.contains("hidden")) return;
if (page !== "friends" && socialTick++ % 5 !== 0) return;
try { await loadSocial(); if (page === "friends") drawSocial(); } catch { }
}, 3000);
async function renderFriends(root) {
root.replaceChildren(h("h1", {}, t("Freunde & Lobby")), h("div", { class: "social", id: "socialRoot" }, h("p", { class: "muted" }, t("Laden …"))));
try { await loadSocial(); }
catch (e) { $("socialRoot").replaceChildren(h("p", { class: "error" }, e.message)); return; }
if (page === "friends") drawSocial();
}
/** Knopf, der eine Aktion ausfuehrt und danach neu laedt. */
function action(label, cls, cmd, args, done) {
const b = h("button", { class: "btn small " + (cls || ""), type: "button" }, label);
b.addEventListener("click", () => busy(b, async () => {
await call(cmd, args || {});
if (done) done();
await loadSocial();
drawSocial();
}));
return b;
}
function drawSocial() {
const root = $("socialRoot");
if (!root) return;
root.replaceChildren(lobbyCard(), friendsCard());
}
function lobbyCard() {
const { lobby, invites } = social.lobby;
const card = h("div", { class: "card" });
for (const i of invites)
card.append(h("div", { class: "invite" }, h("span", {}, h("b", {}, i.owner), t(" lädt dich in die Lobby ein")),
action(t("Beitreten"), "primary", "lobbyJoin", { lobbyId: i.lobbyId }), action(t("Ablehnen"), "", "lobbyDecline", { lobbyId: i.lobbyId })));
if (!lobby) {
card.append(h("h3", {}, t("Lobby")),
h("p", { class: "muted small" }, t("Erstelle eine Lobby und lade Freunde ein. Der Server läuft auf dem BSO-Relay – niemand muss etwas hosten oder Ports freigeben.")),
h("div", { class: "row" }, action(t("Lobby erstellen"), "primary", "lobbyCreate")));
return card;
}
const play = h("button", { class: "btn primary play", type: "button" }, t("SPIELEN"));
// Beim Ersteller startet SPIELEN den Lobby-Server gleich mit, die anderen koennen spielen, sobald er laeuft
play.disabled = !lobby.serverOnline && !lobby.isOwner;
play.addEventListener("click", () => busy(play, async () => {
await call("playLobby", { startServer: lobby.isOwner && !lobby.serverOnline });
toast(t("Bus-Simulator 2012 startet und verbindet mit der Lobby …"));
await loadSocial();
drawSocial();
}));
const state = h("div", { class: "server-state" },
h("span", { class: "dot " + (lobby.serverOnline ? "ok" : "bad") }),
h("span", { style: "flex:1" }, lobby.serverOnline ? t("Server läuft – alle können SPIELEN drücken")
: lobby.isOwner ? t("Drück SPIELEN – der Server startet dann für die ganze Lobby") : t("Warte, bis {0} SPIELEN drückt – dann startet der Server", lobby.owner)),
lobby.isOwner && lobby.serverOnline ? action(t("Server stoppen"), "", "lobbyStop") : null);
const members = h("div", { class: "people" }, ...lobby.members.map((m) => h("div", { class: "person" },
h("span", { class: "dot " + (m.online ? "ok" : "") }),
h("span", { class: "name" }, m.username),
m.isOwner ? h("span", { class: "crown", title: t("Ersteller") }, "★") : null,
lobby.isOwner && !m.isOwner ? action(t("Entfernen"), "ghost", "lobbyKick", { accountId: m.accountId }) : null)));
card.append(h("h3", {}, lobby.isOwner ? t("Deine Lobby") : t("Lobby von {0}", lobby.owner)), state,
h("div", { class: "section-title" }, t("Mitglieder ({0})", lobby.members.length)), members,
lobby.invited.length ? h("p", { class: "muted small" }, t("{0} Einladung(en) offen", lobby.invited.length)) : null,
h("div", { class: "row", style: "justify-content:space-between;margin-top:6px" }, play,
action(lobby.isOwner ? t("Lobby auflösen") : t("Lobby verlassen"), "danger", "lobbyLeave")));
return card;
}
function friendsCard() {
const { friends, incoming, outgoing } = social.friends;
const lobby = social.lobby.lobby;
const name = h("input", { placeholder: t("Benutzername"), maxlength: "32", spellcheck: "false" });
const add = h("button", { class: "btn small primary", type: "button" }, t("Hinzufügen"));
const send = () => busy(add, async () => {
if (!name.value.trim()) return;
await call("friendRequest", { username: name.value.trim() });
toast(t("Anfrage an {0} geschickt", name.value.trim()));
name.value = "";
await loadSocial();
drawSocial();
});
add.addEventListener("click", send);
name.addEventListener("keydown", (e) => { if (e.key === "Enter") send(); });
const card = h("div", { class: "card" }, h("h3", {}, t("Freunde")), h("div", { class: "row" }, name, add));
if (incoming.length)
card.append(h("div", { class: "section-title" }, t("Anfragen")), h("div", { class: "people" }, ...incoming.map((r) => h("div", { class: "person" },
h("span", { class: "name" }, r.username), action(t("Annehmen"), "primary", "friendAccept", { id: r.id }), action(t("Ablehnen"), "", "friendRemove", { id: r.id })))));
card.append(h("div", { class: "section-title" }, t("Freunde ({0} online)", friends.filter((f) => f.online).length)));
if (!friends.length) card.append(h("p", { class: "muted small" }, t("Noch keine Freunde – füge jemanden über seinen BSO-Benutzernamen hinzu.")));
card.append(h("div", { class: "people" }, ...friends.map((f) => {
const inLobby = lobby && lobby.members.some((m) => m.accountId === f.accountId);
const invited = lobby && lobby.invited.includes(f.accountId);
const remove = h("button", { class: "x", type: "button", title: t("Freund entfernen") }, "");
remove.addEventListener("click", () => { if (confirm(t("{0} als Freund entfernen?", f.username))) busy(remove, async () => { await call("friendRemove", { id: f.id }); await loadSocial(); drawSocial(); }); });
return h("div", { class: "person" },
h("span", { class: "dot " + (f.online ? "ok" : "") }), h("span", { class: "name" }, f.username),
lobby && lobby.isOwner && !inLobby ? (invited ? h("span", { class: "muted small" }, t("eingeladen")) : action(t("Einladen"), "", "lobbyInvite", { accountId: f.accountId })) : null,
inLobby ? h("span", { class: "muted small" }, t("in der Lobby")) : null,
remove);
})));
if (outgoing.length)
card.append(h("div", { class: "section-title" }, t("Gesendet")), h("div", { class: "people" }, ...outgoing.map((r) => h("div", { class: "person" },
h("span", { class: "name" }, r.username), h("span", { class: "muted small" }, t("wartet …")), action(t("Zurückziehen"), "ghost", "friendRemove", { id: r.id })))));
return card;
}
// ---------------------------------------------------------------- Neuigkeiten, Einstellungen
async function renderNews(root) {
let list;
try { list = await call("releases"); }
catch (e) { root.replaceChildren(h("h1", {}, t("Neuigkeiten")), h("p", { class: "error" }, e.message)); return; }
if (page !== "news") return;
const names = { mod: "Mod", launcher: "Launcher", server: "Server" };
list = list.filter((r) => r.product !== "server"); // Server laufen jetzt im BSO-Relay
root.replaceChildren(h("h1", {}, t("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 || t("Keine Beschreibung")))) : [h("p", { class: "muted" }, t("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(t("Gespeichert"))));
return h("label", { class: "switch" }, h("span", {}, label, h("small", {}, hint)), box);
};
const folder = h("button", { class: "btn small", type: "button" }, t("Spielordner waehlen"));
folder.addEventListener("click", () => busy(folder, async () => { if ((await call("chooseGameFolder")).changed) { toast(t("Spielordner gespeichert")); showPage("settings"); } }));
const log = h("button", { class: "btn small", type: "button" }, t("Protokoll-Ordner oeffnen"));
log.addEventListener("click", () => call("openLog"));
const uninstall = h("button", { class: "btn small danger", type: "button" }, t("BSO deinstallieren"));
uninstall.addEventListener("click", () => busy(uninstall, () => call("uninstall")));
const langBox = h("div", { class: "lang-switch", "data-lang-switch": true });
// Launcher-Update von Hand (wie beim MSCO Launcher: "Nach Updates suchen" -> "Installieren und neu starten")
const launcherBox = h("div", {});
const check = h("button", { class: "btn small", type: "button" }, t("Nach Updates suchen"));
const install = h("button", { class: "btn small primary", type: "button" }, t("Installieren und neu starten"));
install.addEventListener("click", () => busy(install, async () => {
progressBox = h("div", { class: "progress" }, h("span", { class: "small muted" }), h("div", { class: "bar" }, h("div")));
launcherBox.append(progressBox);
try { await call("installLauncherUpdate"); install.textContent = t("Neustart …"); install.disabled = true; }
catch (e) { progressBox.remove(); throw e; }
}));
check.addEventListener("click", () => busy(check, async () => {
const u = await call("checkLauncherUpdate");
launcherBox.replaceChildren(u.available
? h("div", { class: "row" }, h("b", {}, t("Launcher {0} ist da", u.latest)), install)
: h("span", { class: "muted small" }, u.latest ? t("Der Launcher ist aktuell") : t("Keine Launcher-Version auf dem Server")));
}));
root.replaceChildren(h("h1", {}, t("Einstellungen")), h("div", { class: "settings" },
h("div", { class: "card" },
toggle(t("Launcher automatisch aktualisieren"), t("Beim Start wird eine neue Launcher-Version gleich installiert (er startet von selbst neu)"),
"autoUpdateLauncher", init.autoUpdateLauncher ?? true),
toggle(t("BSO automatisch aktualisieren"), t("Neue BSO-Versionen beim Start des Launchers und vor jedem Spielstart installieren"), "autoUpdateMod", s?.settings.autoUpdateMod ?? true),
toggle(t("Launcher offen lassen"), t("Sonst schliesst er sich nach dem Spielstart"), "keepOpen", s?.settings.keepOpen ?? false),
h("div", { class: "switch" }, h("span", {}, t("Sprache")), langBox)),
h("div", { class: "card" },
h("h3", {}, t("Spiel")),
h("span", { class: "mono muted" }, s?.game.folder || t("Nicht gefunden")),
h("div", { class: "row" }, folder)),
h("div", { class: "card" },
h("h3", {}, "Launcher"),
h("span", { class: "muted small" }, t("Version {0} · Server {1}", init.version, init.master)),
launcherBox,
h("div", { class: "row" }, check, log, uninstall))));
renderLangSwitch(setLang);
}
// ---------------------------------------------------------------- Updates (wie beim MSCO Launcher)
/**
* Beim Start: gibt es einen neueren Launcher, gleich installieren und neu starten (Einstellung "Launcher automatisch
* aktualisieren"). Hoechstens 8 s auf die Pruefung warten - ohne Netz startet der Launcher normal.
*/
async function startupUpdate(init) {
let u;
try {
u = await Promise.race([call("checkLauncherUpdate"), new Promise((_, no) => setTimeout(() => no(new Error("timeout")), 8000))]);
} catch { return false; }
if (!u || !u.available) return false;
if (init.autoUpdateLauncher === false) { toast(t("Launcher {0} ist da – unter Einstellungen installieren", u.latest)); return false; }
const progress = h("div", { class: "progress" }, h("span", { class: "small muted" }), h("div", { class: "bar" }, h("div")));
const text = h("p", { class: "muted" }, t("Launcher wird auf {0} aktualisiert …", u.latest));
const overlay = h("div", { class: "modal-back" }, h("div", { class: "modal update-box" },
h("img", { src: "logo.svg", alt: "" }),
h("h2", {}, t("Launcher {0} ist da", u.latest)),
text, progress,
h("small", { class: "muted" }, t("Der Launcher startet danach von selbst neu."))));
document.body.append(overlay);
progressBox = progress;
try {
await call("installLauncherUpdate");
text.textContent = t("Neustart …");
return true;
} catch (e) {
overlay.remove();
progressBox = null;
toast(t("Launcher-Update fehlgeschlagen: {0}", e.message), true);
return false;
}
}
/** Nach dem Anmelden: eine neue BSO-Version gleich installieren (Einstellung "BSO automatisch aktualisieren"). */
async function autoUpdateMod() {
let s;
try { s = await call("status"); } catch { return; }
if (!s.settings.autoUpdateMod || !s.game.found || s.game.running || !s.mod.updateAvailable || !s.mod.latest) return;
try {
const r = await call("installMod");
toast(t("BSO {0} installiert", r.version));
if (page === "home") showPage("home");
} catch (e) { toast(e.message, true); }
}
// ---------------------------------------------------------------- Start
(async () => {
const init = await call("init");
lang = init.language === "en" ? "en" : "de";
translateStatic();
renderLangSwitch(setLang);
setAuthMode(false);
$("meVersion").textContent = "Launcher " + init.version;
if (init.dev) {
// -dev: gegen die lokale Test-Website - gut sichtbar, damit niemand das verwechselt
const host = init.master.replace(/^https?:\/\//, "");
$("devBadge").textContent = "DEV · " + host;
$("devBadge").classList.remove("hidden");
$("meVersion").textContent += " · DEV";
}
$("authServer").textContent = t("Server: {0}", init.master.replace(/^https?:\/\//, ""));
if (await startupUpdate(init)) return; // neue Version startet gleich
if (!init.loggedIn) { showAuth(); return; }
const me = await call("me");
if (me.loggedIn) showApp(me.username);
else showAuth();
})();