diff --git a/src/EzggLanManager.py b/src/EzggLanManager.py index 26a2589..3fdb22e 100644 --- a/src/EzggLanManager.py +++ b/src/EzggLanManager.py @@ -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, )) diff --git a/src/ezgg_lan_manager/__init__.py b/src/ezgg_lan_manager/__init__.py index a53851c..f781029 100644 --- a/src/ezgg_lan_manager/__init__.py +++ b/src/ezgg_lan_manager/__init__.py @@ -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 diff --git a/src/ezgg_lan_manager/assets/img/games/rl.png b/src/ezgg_lan_manager/assets/img/games/rl.png new file mode 100644 index 0000000..349f589 Binary files /dev/null and b/src/ezgg_lan_manager/assets/img/games/rl.png differ diff --git a/src/ezgg_lan_manager/assets/img/games/teeworlds.png b/src/ezgg_lan_manager/assets/img/games/teeworlds.png new file mode 100644 index 0000000..cc75756 Binary files /dev/null and b/src/ezgg_lan_manager/assets/img/games/teeworlds.png differ diff --git a/src/ezgg_lan_manager/assets/img/games/worms.png b/src/ezgg_lan_manager/assets/img/games/worms.png new file mode 100644 index 0000000..b551388 Binary files /dev/null and b/src/ezgg_lan_manager/assets/img/games/worms.png differ diff --git a/src/ezgg_lan_manager/components/TournamentPageRow.py b/src/ezgg_lan_manager/components/TournamentPageRow.py new file mode 100644 index 0000000..2b17bee --- /dev/null +++ b/src/ezgg_lan_manager/components/TournamentPageRow.py @@ -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 + ) diff --git a/src/ezgg_lan_manager/pages/TournamentDetailsPage.py b/src/ezgg_lan_manager/pages/TournamentDetailsPage.py new file mode 100644 index 0000000..40da0fb --- /dev/null +++ b/src/ezgg_lan_manager/pages/TournamentDetailsPage.py @@ -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 + ) diff --git a/src/ezgg_lan_manager/pages/TournamentsPage.py b/src/ezgg_lan_manager/pages/TournamentsPage.py index 41973de..7875305 100644 --- a/src/ezgg_lan_manager/pages/TournamentsPage.py +++ b/src/ezgg_lan_manager/pages/TournamentsPage.py @@ -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 diff --git a/src/ezgg_lan_manager/pages/__init__.py b/src/ezgg_lan_manager/pages/__init__.py index deee520..0788905 100644 --- a/src/ezgg_lan_manager/pages/__init__.py +++ b/src/ezgg_lan_manager/pages/__init__.py @@ -20,3 +20,4 @@ from .ManageUsersPage import ManageUsersPage from .ManageCateringPage import ManageCateringPage from .ManageTournamentsPage import ManageTournamentsPage from .OverviewPage import OverviewPage +from .TournamentDetailsPage import TournamentDetailsPage diff --git a/src/ezgg_lan_manager/services/TournamentService.py b/src/ezgg_lan_manager/services/TournamentService.py new file mode 100644 index 0000000..96c08df --- /dev/null +++ b/src/ezgg_lan_manager/services/TournamentService.py @@ -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 diff --git a/src/ezgg_lan_manager/types/Tournament.py b/src/ezgg_lan_manager/types/Tournament.py index f83ebe3..ad7b2fd 100644 --- a/src/ezgg_lan_manager/types/Tournament.py +++ b/src/ezgg_lan_manager/types/Tournament.py @@ -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") diff --git a/src/ezgg_lan_manager/types/TournamentBase.py b/src/ezgg_lan_manager/types/TournamentBase.py index 5b79e23..0d0cd5e 100644 --- a/src/ezgg_lan_manager/types/TournamentBase.py +++ b/src/ezgg_lan_manager/types/TournamentBase.py @@ -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