Files
BSO-Launcher/ui/app.js
T
MrMahoandClaude Opus 5.5 6ec2714a15 Launcher 0.3.3: Freunde-Eingabe bleibt, Lobby eines Freundes beitreten
Das Feld Benutzername wird beim Aktualisieren nicht mehr neu angelegt (Text, Fokus und Cursor bleiben),
neu gezeichnet wird nur bei Aenderungen. Beitreten-Knopf bei Freunden mit Lobby und Liste der Freundes-Lobbys.

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

563 lines
27 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;
const before = JSON.stringify(social);
try {
await loadSocial();
if (page === "friends" && JSON.stringify(social) !== before) drawSocial(); // nur neu zeichnen, wenn sich was geaendert hat
} 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;
}
// Eingabefeld "Freund hinzufuegen" bleibt ueber das Neuzeichnen hinweg dasselbe - sonst waere Getipptes weg
const friendName = h("input", { maxlength: "32", spellcheck: "false" });
function drawSocial() {
const root = $("socialRoot");
if (!root) return;
const typing = document.activeElement === friendName;
const [start, end] = [friendName.selectionStart, friendName.selectionEnd];
root.replaceChildren(lobbyCard(), friendsCard());
if (typing) { friendName.focus(); friendName.setSelectionRange(start, end); }
}
/** Der Lobby eines Freundes beitreten - auch ohne Einladung. Wer selbst eine Lobby erstellt hat, loest sie dabei auf. */
function joinButton(fl) {
const b = h("button", { class: "btn small primary", type: "button" }, t("Beitreten"));
b.disabled = fl.full;
if (fl.full) b.title = t("Die Lobby ist voll");
b.addEventListener("click", () => {
const mine = social.lobby.lobby;
if (mine && mine.isOwner && !confirm(t("Deine Lobby wird dabei aufgelöst. Trotzdem {0} beitreten?", fl.owner))) return;
busy(b, async () => { await call("lobbyJoin", { lobbyId: fl.lobbyId }); await loadSocial(); drawSocial(); });
});
return b;
}
function lobbyCard() {
const { lobby, invites } = social.lobby;
const friendLobbies = social.lobby.friendLobbies || [];
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")));
if (friendLobbies.length)
card.append(h("div", { class: "section-title" }, t("Lobbys deiner Freunde")),
h("div", { class: "people" }, ...friendLobbies.map((fl) => h("div", { class: "person" },
h("span", { class: "dot " + (fl.serverOnline ? "ok" : "") }),
h("span", { class: "name" }, t("Lobby von {0}", fl.owner)),
h("span", { class: "muted small" }, t("{0} Spieler", fl.members)),
joinButton(fl)))));
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 = friendName;
name.placeholder = t("Benutzername");
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.onkeydown = (e) => { if (e.key === "Enter") send(); }; // onkeydown statt addEventListener: das Feld bleibt, der Knopf ist neu
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 theirLobby = (social.lobby.friendLobbies || []).find((fl) => fl.ownerId === 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,
!inLobby && theirLobby ? joinButton(theirLobby) : 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();
})();