new: dev: initialize the GTK client #11
This commit is contained in:
185
gtk-client/mineseeker/ui/lobby_page.py
Normal file
185
gtk-client/mineseeker/ui/lobby_page.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
This file is part of the SplendidBear Websites' projects.
|
||||
|
||||
Copyright (c) 2026 @ www.splendidbear.org
|
||||
|
||||
For the full copyright and license information, please view the LICENSE
|
||||
file that was distributed with this source code.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from collections.abc import Callable
|
||||
|
||||
import gi
|
||||
gi.require_version("Gtk", "4.0")
|
||||
gi.require_version("Adw", "1")
|
||||
from gi.repository import Gtk, Adw, GLib
|
||||
|
||||
from mineseeker.api import game as game_api
|
||||
from mineseeker import assets
|
||||
from mineseeker.state import session as session_mod
|
||||
|
||||
|
||||
class LobbyPage(Gtk.Box):
|
||||
"""
|
||||
Lobby screen — shows waiting players and a "New Game" button.
|
||||
|
||||
Flow:
|
||||
- "New Game" → fetch token → start game → on_game_start()
|
||||
- Click a waiting player → challenge them → on_game_start() when accepted
|
||||
"""
|
||||
|
||||
def __init__(self, on_game_start: Callable[[str, str, str], None]) -> None:
|
||||
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
self._on_game_start = on_game_start
|
||||
self._waiting: list[dict] = []
|
||||
|
||||
# Header bar action area
|
||||
header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
header_box.set_margin_top(12)
|
||||
header_box.set_margin_bottom(12)
|
||||
header_box.set_margin_start(16)
|
||||
header_box.set_margin_end(16)
|
||||
|
||||
title = Gtk.Label(label="Lobby")
|
||||
title.add_css_class("title-2")
|
||||
title.set_hexpand(True)
|
||||
title.set_xalign(0)
|
||||
header_box.append(title)
|
||||
|
||||
self._refresh_btn = Gtk.Button(label="Refresh")
|
||||
self._refresh_btn.connect("clicked", lambda *_: self.refresh())
|
||||
header_box.append(self._refresh_btn)
|
||||
|
||||
new_game_btn = Gtk.Button(label="New Game")
|
||||
new_game_btn.add_css_class("suggested-action")
|
||||
new_game_btn.connect("clicked", self._on_new_game)
|
||||
header_box.append(new_game_btn)
|
||||
|
||||
self.append(header_box)
|
||||
self.append(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL))
|
||||
|
||||
# Waiting players list
|
||||
scrolled = Gtk.ScrolledWindow()
|
||||
scrolled.set_vexpand(True)
|
||||
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
|
||||
self._list_box = Gtk.ListBox()
|
||||
self._list_box.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||
self._list_box.add_css_class("boxed-list")
|
||||
self._list_box.set_margin_top(12)
|
||||
self._list_box.set_margin_bottom(12)
|
||||
self._list_box.set_margin_start(16)
|
||||
self._list_box.set_margin_end(16)
|
||||
|
||||
scrolled.set_child(self._list_box)
|
||||
self.append(scrolled)
|
||||
|
||||
self._status_label = Gtk.Label(label="No players waiting.")
|
||||
self._status_label.add_css_class("dim-label")
|
||||
self._status_label.set_margin_top(24)
|
||||
self._status_label.set_visible(True)
|
||||
self.append(self._status_label)
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""Fetch waiting players list from the server."""
|
||||
self._refresh_btn.set_sensitive(False)
|
||||
threading.Thread(target=self._do_refresh, daemon=True).start()
|
||||
|
||||
def _do_refresh(self) -> None:
|
||||
try:
|
||||
waiting = game_api.waiting()
|
||||
GLib.idle_add(self._update_list, waiting)
|
||||
except Exception:
|
||||
GLib.idle_add(self._refresh_btn.set_sensitive, True)
|
||||
|
||||
def _update_list(self, waiting: list[dict]) -> bool:
|
||||
self._waiting = waiting
|
||||
# Clear existing rows
|
||||
while True:
|
||||
row = self._list_box.get_first_child()
|
||||
if row is None:
|
||||
break
|
||||
self._list_box.remove(row)
|
||||
|
||||
my_assoc = session_mod.get().game_assoc
|
||||
|
||||
for player in waiting:
|
||||
if player["gameAssoc"] == my_assoc:
|
||||
continue # don't show ourselves
|
||||
row = self._make_player_row(player)
|
||||
self._list_box.append(row)
|
||||
|
||||
has_players = bool([p for p in waiting if p.get("gameAssoc") != my_assoc])
|
||||
self._status_label.set_visible(not has_players)
|
||||
self._refresh_btn.set_sensitive(True)
|
||||
return GLib.SOURCE_REMOVE
|
||||
|
||||
def _make_player_row(self, player: dict) -> Adw.ActionRow:
|
||||
row = Adw.ActionRow()
|
||||
row.set_title(player.get("name", "Guest"))
|
||||
row.set_subtitle(f"Waiting since {player.get('since', '')[:19].replace('T', ' ')}")
|
||||
|
||||
challenge_btn = Gtk.Button(label="Challenge")
|
||||
challenge_btn.add_css_class("flat")
|
||||
challenge_btn.set_valign(Gtk.Align.CENTER)
|
||||
challenge_btn.connect(
|
||||
"clicked",
|
||||
lambda _btn, p=player: self._on_challenge(p),
|
||||
)
|
||||
row.add_suffix(challenge_btn)
|
||||
return row
|
||||
|
||||
def _on_new_game(self, *_) -> None:
|
||||
threading.Thread(target=self._do_new_game, daemon=True).start()
|
||||
|
||||
def _do_new_game(self) -> None:
|
||||
try:
|
||||
token_data = game_api.fetch_token()
|
||||
game_assoc = token_data["gameAssoc"]
|
||||
mercure_jwt = token_data["mercureJwt"]
|
||||
|
||||
sess = session_mod.get()
|
||||
sess.game_assoc = game_assoc
|
||||
sess.mercure_jwt = mercure_jwt
|
||||
sess.color = "red" # first player always red
|
||||
|
||||
# Load images while we wait for an opponent
|
||||
assets.load_images()
|
||||
|
||||
GLib.idle_add(self._on_game_start, game_assoc, mercure_jwt, "red")
|
||||
except Exception as e:
|
||||
GLib.idle_add(self._show_error_toast, str(e))
|
||||
|
||||
def _on_challenge(self, player: dict) -> None:
|
||||
threading.Thread(
|
||||
target=self._do_challenge, args=(player,), daemon=True
|
||||
).start()
|
||||
|
||||
def _do_challenge(self, player: dict) -> None:
|
||||
try:
|
||||
token_data = game_api.fetch_token()
|
||||
game_assoc = token_data["gameAssoc"]
|
||||
mercure_jwt = token_data["mercureJwt"]
|
||||
|
||||
sess = session_mod.get()
|
||||
sess.game_assoc = game_assoc
|
||||
sess.mercure_jwt = mercure_jwt
|
||||
|
||||
game_api.challenge(
|
||||
target_game_assoc=player["gameAssoc"],
|
||||
challenger_game_assoc=game_assoc,
|
||||
)
|
||||
|
||||
assets.load_images()
|
||||
# GamePage will determine color from subscribe payload
|
||||
GLib.idle_add(self._on_game_start, game_assoc, mercure_jwt, "")
|
||||
except Exception as e:
|
||||
GLib.idle_add(self._show_error_toast, str(e))
|
||||
|
||||
def _show_error_toast(self, message: str) -> bool:
|
||||
# Find the nearest Adw.ToastOverlay ancestor if available, otherwise print
|
||||
print(f"[LobbyPage] Error: {message}")
|
||||
return GLib.SOURCE_REMOVE
|
||||
Reference in New Issue
Block a user