From 6ec2714a15ce92d23f247f7c4eb7abcbac40f5a7 Mon Sep 17 00:00:00 2001 From: MrMaho Date: Wed, 28 Oct 2026 21:29:22 +0100 Subject: [PATCH] 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 --- BSO.Launcher.csproj | 2 +- CHANGELOG.md | 4 ++++ ui/app.js | 40 +++++++++++++++++++++++++++++++++++++--- ui/i18n.js | 2 ++ 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/BSO.Launcher.csproj b/BSO.Launcher.csproj index 8db1bf6..f9b076c 100644 --- a/BSO.Launcher.csproj +++ b/BSO.Launcher.csproj @@ -8,7 +8,7 @@ enable BSO.Launcher BSO-Launcher - 0.3.2 + 0.3.3 MBM.wtf BSO Launcher BSO Launcher - Bus Simulator Online diff --git a/CHANGELOG.md b/CHANGELOG.md index d4315ac..7112e1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ Neue Version: hier einen Abschnitt `## ` anlegen, `` in `BSO.Launcher.csproj` erhöhen, dann `dotnet publish -c Release -o publish` – lädt die Version hoch; der Abschnitt wird der Changelog auf der Website und im Launcher. +## 0.3.3 +- Freunde: Was man ins Feld „Benutzername“ tippt, verschwindet nicht mehr nach ein paar Sekunden +- Lobby eines Freundes beitreten, auch ohne Einladung („Beitreten“ beim Freund und unter „Lobbys deiner Freunde“) + ## 0.3.2 - Auto-Update wie beim MSCO Launcher: ein neuer Launcher installiert sich beim Start sofort selbst und startet neu, neue BSO-Versionen kommen beim Start (beides in den Einstellungen abschaltbar) diff --git a/ui/app.js b/ui/app.js index 438bb5c..a35b9dc 100644 --- a/ui/app.js +++ b/ui/app.js @@ -275,7 +275,11 @@ 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 { } + 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) { @@ -297,14 +301,34 @@ function action(label, cls, cmd, args, done) { 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")), @@ -314,6 +338,13 @@ function lobbyCard() { 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; } @@ -350,7 +381,8 @@ function lobbyCard() { 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 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; @@ -361,7 +393,7 @@ function friendsCard() { drawSocial(); }); add.addEventListener("click", send); - name.addEventListener("keydown", (e) => { if (e.key === "Enter") 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) @@ -373,12 +405,14 @@ function friendsCard() { 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); }))); diff --git a/ui/i18n.js b/ui/i18n.js index 12b375a..ea20dd4 100644 --- a/ui/i18n.js +++ b/ui/i18n.js @@ -38,6 +38,8 @@ const EN = { "Server läuft – alle können SPIELEN drücken": "Server is running – everyone can press PLAY", "Drück SPIELEN – der Server startet dann für die ganze Lobby": "Press PLAY – the server then starts for the whole lobby", "Warte, bis {0} SPIELEN drückt – dann startet der Server": "Waiting for {0} to press PLAY – then the server starts", "Server stoppen": "Stop server", + "Lobbys deiner Freunde": "Your friends' lobbies", "{0} Spieler": "{0} players", "Die Lobby ist voll": "The lobby is full", + "Deine Lobby wird dabei aufgelöst. Trotzdem {0} beitreten?": "Your lobby will be closed. Join {0} anyway?", "Ersteller": "Owner", "Entfernen": "Remove", "Deine Lobby": "Your lobby", "Lobby von {0}": "{0}'s lobby", "Mitglieder ({0})": "Members ({0})", "{0} Einladung(en) offen": "{0} invitation(s) pending", "Lobby auflösen": "Close lobby", "Lobby verlassen": "Leave lobby", "Hinzufügen": "Add", "Anfrage an {0} geschickt": "Request sent to {0}", "Anfragen": "Requests", "Annehmen": "Accept",