186 lines
6.4 KiB
Python
186 lines
6.4 KiB
Python
"""
|
|
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
|