Add Tournaments UI #32

Merged
Typhus merged 10 commits from feature/add-tournaments-ui into main 2026-02-03 23:00:58 +00:00
12 changed files with 271 additions and 18 deletions
Showing only changes of commit b6ef2b5995 - Show all commits

View File

@ -30,7 +30,7 @@ if __name__ == "__main__":
corner_radius_large=0,
font=Font(from_root("src/ezgg_lan_manager/assets/fonts/joystix.otf"))
)
default_attachments = [LocalData()]
default_attachments: list = [LocalData()]
default_attachments.extend(init_services())
lan_info = default_attachments[3].get_lan_info()
@ -161,6 +161,11 @@ if __name__ == "__main__":
name="DbErrorPage",
url_segment="db-error",
build=pages.DbErrorPage,
),
ComponentPage(
name="TournamentDetailsPage",
url_segment="tournament",
build=pages.TournamentDetailsPage,
)
],
theme=theme,
@ -188,5 +193,5 @@ if __name__ == "__main__":
sys.exit(app.run_as_web_server(
host="0.0.0.0",
port=8000,
port=8001,
))

View File

@ -13,11 +13,12 @@ from src.ezgg_lan_manager.services.NewsService import NewsService
from src.ezgg_lan_manager.services.ReceiptPrintingService import ReceiptPrintingService
from src.ezgg_lan_manager.services.SeatingService import SeatingService
from src.ezgg_lan_manager.services.TicketingService import TicketingService
from src.ezgg_lan_manager.services.TournamentService import TournamentService
from src.ezgg_lan_manager.services.UserService import UserService
from src.ezgg_lan_manager.types import *
# Inits services in the correct order
def init_services() -> tuple[AccountingService, CateringService, ConfigurationService, DatabaseService, MailingService, NewsService, SeatingService, TicketingService, UserService, LocalDataService, ReceiptPrintingService]:
def init_services() -> tuple[AccountingService, CateringService, ConfigurationService, DatabaseService, MailingService, NewsService, SeatingService, TicketingService, UserService, LocalDataService, ReceiptPrintingService, TournamentService]:
logging.basicConfig(level=logging.DEBUG)
configuration_service = ConfigurationService(from_root("config.toml"))
db_service = DatabaseService(configuration_service.get_database_configuration())
@ -30,6 +31,7 @@ def init_services() -> tuple[AccountingService, CateringService, ConfigurationSe
receipt_printing_service = ReceiptPrintingService(seating_service, configuration_service.get_receipt_printing_configuration(), configuration_service.DEV_MODE_ACTIVE)
catering_service = CateringService(db_service, accounting_service, user_service, receipt_printing_service)
local_data_service = LocalDataService()
tournament_service = TournamentService(db_service, user_service)
return accounting_service, catering_service, configuration_service, db_service, mailing_service, news_service, seating_service, ticketing_service, user_service, local_data_service, receipt_printing_service
return accounting_service, catering_service, configuration_service, db_service, mailing_service, news_service, seating_service, ticketing_service, user_service, local_data_service, receipt_printing_service, tournament_service

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -0,0 +1,60 @@
from typing import Literal, Callable
from rio import Component, PointerEventListener, Rectangle, Image, Text, Tooltip, TextStyle, Color, Icon, Row, PointerEvent
from from_root import from_root
from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus
class TournamentPageRow(Component):
tournament_id: int
tournament_name: str
game_image_name: str
current_participants: int
max_participants: int
tournament_status: TournamentStatus
clicked_cb: Callable
def handle_click(self, _: PointerEvent) -> None:
self.clicked_cb(self.tournament_id)
def determine_tournament_status_icon_color_and_text(self) -> tuple[str, Literal["success", "warning", "danger"], str]:
if self.tournament_status == TournamentStatus.OPEN:
return "material/lock_open", "success", "Anmeldung geöffnet"
elif self.tournament_status == TournamentStatus.CLOSED:
return "material/lock", "danger", "Anmeldung geschlossen"
elif self.tournament_status == TournamentStatus.ONGOING:
return "material/autoplay", "warning", "Turnier läuft"
elif self.tournament_status == TournamentStatus.COMPLETED:
return "material/check_circle", "success", "Turnier beendet"
elif self.tournament_status == TournamentStatus.CANCELED:
return "material/cancel", "danger", "Turnier abgesagt"
elif self.tournament_status == TournamentStatus.INVITE_ONLY:
return "material/person_cancel", "warning", "Teilnahme nur per Einladung"
else:
raise RuntimeError(f"Unknown tournament status: {str(self.tournament_status)}")
def build(self) -> Component:
icon_name, color, text = self.determine_tournament_status_icon_color_and_text()
return PointerEventListener(
content=Rectangle(
content=Row(
Image(image=from_root(f"src/ezgg_lan_manager/assets/img/games/{self.game_image_name}")),
Text(self.tournament_name, style=TextStyle(fill=self.session.theme.background_color, font_size=1)),
Text(f"{self.current_participants}/{self.max_participants}", style=TextStyle(fill=self.session.theme.background_color, font_size=1), justify="right", margin_right=0.5),
Tooltip(anchor=Icon(icon_name, min_width=1, min_height=1, fill=color), position="top",
tip=Text(text, style=TextStyle(fill=self.session.theme.background_color, font_size=0.7))),
proportions=[1, 4, 1, 1],
margin=.5
),
fill=self.session.theme.hud_color,
margin=1,
margin_bottom=0,
stroke_color=Color.TRANSPARENT,
stroke_width=0.2,
hover_stroke_color=self.session.theme.background_color,
cursor="pointer"
),
on_press=self.handle_click
)

View File

@ -0,0 +1,65 @@
from typing import Optional
from rio import Column, Component, event, TextStyle, Text, Rectangle, Row, Image, Icon, Tooltip, Spacer, Color, PointerEventListener, ProgressCircle
from src.ezgg_lan_manager import ConfigurationService, TournamentService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.TournamentPageRow import TournamentPageRow
from src.ezgg_lan_manager.types.Tournament import Tournament
from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus
class TournamentDetailsPage(Component):
tournament: Optional[Tournament] = None
@event.on_populate
async def on_populate(self) -> None:
try:
tournament_id = int(self.session.active_page_url.query_string.split("id=")[-1])
except (ValueError, AttributeError, TypeError):
tournament_id = None
if tournament_id is not None:
self.tournament = await self.session[TournamentService].get_tournament_by_id(tournament_id)
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - {self.tournament.name}")
else:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turniere")
def build(self) -> Component:
if not self.tournament:
return Column(
MainViewContentBox(
Column(
Column(
ProgressCircle(
color="secondary",
align_x=0.5,
margin_top=0,
margin_bottom=0
),
min_height=10
),
Spacer(min_height=1)
)
),
align_y = 0
)
return Column(
MainViewContentBox(
Column(
Text(
text=self.tournament.name,
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
),
Spacer(min_height=1)
)
),
align_y=0
)

View File

@ -1,15 +1,50 @@
from rio import Column, Component, event, TextStyle, Text
from rio import Column, Component, event, TextStyle, Text, Spacer, ProgressCircle
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager import ConfigurationService, TournamentService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.TournamentPageRow import TournamentPageRow
from src.ezgg_lan_manager.types.Tournament import Tournament
class TournamentsPage(Component):
tournament_data: list[Tournament] = []
@event.on_populate
async def on_populate(self) -> None:
self.tournament_data = await self.session[TournamentService].get_tournaments()
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turniere")
def tournament_clicked(self, tournament_id: int) -> None:
self.session.navigate_to(f"tournament?id={tournament_id}")
def build(self) -> Component:
tournament_page_rows = []
for tournament in self.tournament_data:
tournament_page_rows.append(
TournamentPageRow(
tournament.id,
tournament.name,
tournament.game_title.image_name,
len(tournament.participants),
tournament.max_participants,
tournament.status,
self.tournament_clicked
)
)
if len(self.tournament_data) == 0:
content = [Column(
ProgressCircle(
color="secondary",
align_x=0.5,
margin_top=0,
margin_bottom=0
),
min_height=10
)]
else:
content = tournament_page_rows
return Column(
MainViewContentBox(
Column(
@ -20,18 +55,11 @@ class TournamentsPage(Component):
font_size=1.2
),
margin_top=2,
margin_bottom=0,
margin_bottom=2,
align_x=0.5
),
Text(
text="Aktuell ist noch kein Turnierplan hinterlegt.",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.9
),
margin=1,
overflow="wrap"
)
*content,
Spacer(min_height=1)
)
),
align_y=0

View File

@ -20,3 +20,4 @@ from .ManageUsersPage import ManageUsersPage
from .ManageCateringPage import ManageCateringPage
from .ManageTournamentsPage import ManageTournamentsPage
from .OverviewPage import OverviewPage
from .TournamentDetailsPage import TournamentDetailsPage

View File

@ -0,0 +1,86 @@
from asyncio import sleep
from datetime import datetime
from typing import Optional
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
from src.ezgg_lan_manager.services.UserService import UserService
from src.ezgg_lan_manager.types.Tournament import Tournament
from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus
class TournamentService:
def __init__(self, db_service: DatabaseService, user_service: UserService) -> None:
self._db_service = db_service
self._user_service = user_service
# This overrides the database access and is meant for easy development.
# Set to None before merging back into main.
self._dev_data = [
Tournament(
0,
"Teeworlds 2vs2",
GameTitle(
"Teeworlds",
"Teeworlds is a free online multiplayer game, available for all major operating systems. Battle with up to 16 players in a variety of game modes.",
"https://store.steampowered.com/app/380840/Teeworlds/",
"teeworlds.png"
),
TournamentFormat.SINGLE_ELIMINATION_BO_3,
datetime(2026, 5, 8, 18, 0, 0),
TournamentStatus.OPEN,
[],
None,
[],
32
),
Tournament(
1,
"Rocket League 3vs3",
GameTitle(
"Rocket League",
"Rocket League is a high-powered hybrid of arcade-style soccer and vehicular mayhem with easy-to-understand controls and fluid, physics-driven competition.",
"https://steamcommunity.com/app/252950",
"rl.png"
),
TournamentFormat.SINGLE_ELIMINATION_BO_3,
datetime(2026, 5, 8, 18, 0, 0),
TournamentStatus.OPEN,
[],
None,
[],
8
),
Tournament(
2,
"Worms Armageddon 1vs1",
GameTitle(
"Worms Armageddon",
"2D turn-based artillery strategy game.",
"https://store.steampowered.com/app/217200/Worms_Armageddon/",
"worms.png"
),
TournamentFormat.SINGLE_ELIMINATION_BO_1,
datetime(2026, 5, 8, 18, 30, 0),
TournamentStatus.OPEN,
[],
None,
[],
16
)
]
async def get_tournaments(self) -> list[Tournament]:
# Fake DB lookup delay
await sleep(1)
if self._dev_data is not None:
return self._dev_data
return [] # ToDo: Implement database polling
async def get_tournament_by_id(self, tournament_id: int) -> Optional[Tournament]:
await sleep(1)
try:
return self._dev_data[tournament_id]
except IndexError:
return None

View File

@ -18,7 +18,8 @@ class Tournament:
status: TournamentStatus,
participants: list[Participant],
matches: Optional[tuple[Match]],
rounds: list[list[Match]]) -> None:
rounds: list[list[Match]],
max_participants: int) -> None:
self._id = id_
self._name = name
self._game_title = game_title
@ -28,6 +29,7 @@ class Tournament:
self._participants = participants
self._matches = matches
self._rounds = rounds
self._max_participants = max_participants
@property
def id(self) -> int:
@ -69,6 +71,10 @@ class Tournament:
def matches(self) -> list[Match]:
return self._matches if self._matches else []
@property
def max_participants(self) -> int:
return self._max_participants
def add_participant(self, participant: Participant) -> None:
if participant.id in (p.id for p in self._participants):
raise TournamentError(f"Participant with ID {participant.id} already registered for tournament")

View File

@ -7,7 +7,7 @@ class GameTitle:
name: str
description: str
web_link: str
image_name: str # Name of the image in assets/img/games
class TournamentFormat(Enum):
SINGLE_ELIMINATION_BO_1 = 1