Add tournaments UI - Prototype #1

This commit is contained in:
David Rodenkirchen 2026-01-31 18:33:28 +01:00
parent ff5d715a4e
commit b6ef2b5995
12 changed files with 271 additions and 18 deletions

View File

@ -30,7 +30,7 @@ if __name__ == "__main__":
corner_radius_large=0, corner_radius_large=0,
font=Font(from_root("src/ezgg_lan_manager/assets/fonts/joystix.otf")) font=Font(from_root("src/ezgg_lan_manager/assets/fonts/joystix.otf"))
) )
default_attachments = [LocalData()] default_attachments: list = [LocalData()]
default_attachments.extend(init_services()) default_attachments.extend(init_services())
lan_info = default_attachments[3].get_lan_info() lan_info = default_attachments[3].get_lan_info()
@ -161,6 +161,11 @@ if __name__ == "__main__":
name="DbErrorPage", name="DbErrorPage",
url_segment="db-error", url_segment="db-error",
build=pages.DbErrorPage, build=pages.DbErrorPage,
),
ComponentPage(
name="TournamentDetailsPage",
url_segment="tournament",
build=pages.TournamentDetailsPage,
) )
], ],
theme=theme, theme=theme,
@ -188,5 +193,5 @@ if __name__ == "__main__":
sys.exit(app.run_as_web_server( sys.exit(app.run_as_web_server(
host="0.0.0.0", 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.ReceiptPrintingService import ReceiptPrintingService
from src.ezgg_lan_manager.services.SeatingService import SeatingService from src.ezgg_lan_manager.services.SeatingService import SeatingService
from src.ezgg_lan_manager.services.TicketingService import TicketingService 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.services.UserService import UserService
from src.ezgg_lan_manager.types import * from src.ezgg_lan_manager.types import *
# Inits services in the correct order # 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) logging.basicConfig(level=logging.DEBUG)
configuration_service = ConfigurationService(from_root("config.toml")) configuration_service = ConfigurationService(from_root("config.toml"))
db_service = DatabaseService(configuration_service.get_database_configuration()) 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) 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) catering_service = CateringService(db_service, accounting_service, user_service, receipt_printing_service)
local_data_service = LocalDataService() 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.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.TournamentPageRow import TournamentPageRow
from src.ezgg_lan_manager.types.Tournament import Tournament
class TournamentsPage(Component): class TournamentsPage(Component):
tournament_data: list[Tournament] = []
@event.on_populate @event.on_populate
async def on_populate(self) -> None: 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") 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: 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( return Column(
MainViewContentBox( MainViewContentBox(
Column( Column(
@ -20,18 +55,11 @@ class TournamentsPage(Component):
font_size=1.2 font_size=1.2
), ),
margin_top=2, margin_top=2,
margin_bottom=0, margin_bottom=2,
align_x=0.5 align_x=0.5
), ),
Text( *content,
text="Aktuell ist noch kein Turnierplan hinterlegt.", Spacer(min_height=1)
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.9
),
margin=1,
overflow="wrap"
)
) )
), ),
align_y=0 align_y=0

View File

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

View File

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