import logging from asyncio import sleep from functools import partial from typing import Optional, Union, Literal from from_root import from_root from rio import Column, Component, event, TextStyle, Text, Row, Image, Spacer, ProgressCircle, Button, Checkbox, ThemeContextSwitcher, Link, Revealer, PointerEventListener, \ PointerEvent, Rectangle, Color, Popup, Dropdown from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService, TicketingService, TeamService from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox from src.ezgg_lan_manager.components.TournamentDetailsInfoRow import TournamentDetailsInfoRow from src.ezgg_lan_manager.types.DateUtil import weekday_to_display_text from src.ezgg_lan_manager.types.Team import Team, TeamStatus from src.ezgg_lan_manager.types.Tournament import Tournament from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus, tournament_status_to_display_text, tournament_format_to_display_texts, ParticipantType from src.ezgg_lan_manager.types.User import User from src.ezgg_lan_manager.types.UserSession import UserSession logger = logging.getLogger(__name__.split(".")[-1]) class TournamentDetailsPage(Component): tournament: Optional[Union[Tournament, str]] = None rules_accepted: bool = False user: Optional[User] = None user_teams: list[Team] = [] loading: bool = False participant_revealer_open: bool = False current_tournament_user_or_team_list: Union[list[User], list[Team]] = [] team_dialog_open: bool = False team_register_options: dict[str, Optional[Team]] = {"": None} team_selected_for_register: Optional[Team] = None # State for message above register button message: str = "" is_success: bool = False @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) if self.tournament is not None: await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - {self.tournament.name}") if self.tournament.participant_type == ParticipantType.PLAYER: self.current_tournament_user_or_team_list = await self.session[TournamentService].get_users_from_participant_list(self.tournament.participants) elif self.tournament.participant_type == ParticipantType.TEAM: self.current_tournament_user_or_team_list = await self.session[TournamentService].get_teams_from_participant_list(self.tournament.participants) else: await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turniere") try: user_id = self.session[UserSession].user_id self.user = await self.session[UserService].get_user(user_id) self.user_teams = await self.session[TeamService].get_teams_for_user_by_id(user_id) except KeyError: self.user = None self.user_teams = [] self.loading_done() @staticmethod async def artificial_delay() -> None: await sleep(0.8) # https://medium.com/design-bootcamp/ux-psychology-of-artificial-waiting-enhancing-user-experiences-through-deliberate-delays-d7822faf3930 async def update(self) -> None: self.tournament = await self.session[TournamentService].get_tournament_by_id(self.tournament.id) if self.tournament is None: return if self.tournament.participant_type == ParticipantType.PLAYER: self.current_tournament_user_or_team_list = await self.session[TournamentService].get_users_from_participant_list(self.tournament.participants) elif self.tournament.participant_type == ParticipantType.TEAM: self.current_tournament_user_or_team_list = await self.session[TournamentService].get_teams_from_participant_list(self.tournament.participants) def open_close_participant_revealer(self, _: PointerEvent) -> None: self.participant_revealer_open = not self.participant_revealer_open async def register_pressed(self) -> None: self.loading = True if not self.user: return user_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id) if user_ticket is None: self.is_success = False self.message = "Turnieranmeldung nur mit Ticket" else: # Register single player if self.tournament.participant_type == ParticipantType.PLAYER: try: await self.session[TournamentService].register_user_for_tournament(self.user.user_id, self.tournament.id) await self.artificial_delay() self.is_success = True self.message = f"Erfolgreich angemeldet!" except Exception as e: logger.error(e) self.is_success = False self.message = f"Fehler: {e}" # Register team elif self.tournament.participant_type == ParticipantType.TEAM: try: team_register_options = {"": None} for team in self.user_teams: if team.members[self.user] == TeamStatus.OFFICER or team.members[self.user] == TeamStatus.LEADER: team_register_options[team.name] = team if team_register_options: self.team_register_options = team_register_options else: self.team_register_options = {"": None} except StopIteration as e: logger.error(f"Error trying to teams to register: {e}") self.team_dialog_open = True return # Model should handle loading state now else: pass await self.update() self.loading = False async def on_team_register_confirmed(self) -> None: if self.team_selected_for_register is None: await self.on_team_register_canceled() return try: await self.session[TournamentService].register_team_for_tournament(self.team_selected_for_register.id, self.tournament.id) await self.artificial_delay() self.is_success = True self.message = f"Erfolgreich angemeldet!" self.team_dialog_open = False self.team_selected_for_register = None except Exception as e: logger.error(e) self.message = f"Fehler: {e}" self.is_success = False await self.update() self.loading = False async def on_team_register_canceled(self) -> None: self.team_dialog_open = False self.team_selected_for_register = None self.loading = False async def unregister_pressed(self, team: Optional[Team] = None) -> None: self.loading = True if not self.user: return try: if self.tournament.participant_type == ParticipantType.PLAYER: await self.session[TournamentService].unregister_user_from_tournament(self.user.user_id, self.tournament.id) elif self.tournament.participant_type == ParticipantType.TEAM: if team.members[self.user] == TeamStatus.OFFICER or team.members[self.user] == TeamStatus.LEADER: await self.session[TournamentService].unregister_team_from_tournament(team.id, self.tournament.id) else: raise PermissionError("Nur Leiter und Offiziere können das Team abmelden") await self.artificial_delay() self.is_success = True self.message = f"Erfolgreich abgemeldet!" except Exception as e: self.is_success = False self.message = f"Fehler: {e}" await self.update() self.loading = False async def tree_button_clicked(self) -> None: pass # ToDo: Implement tournament tree view def loading_done(self) -> None: if self.tournament is None: self.tournament = "Turnier konnte nicht gefunden werden" def build(self) -> Component: if self.tournament is None: content = Column( ProgressCircle( color="secondary", align_x=0.5, margin_top=0, margin_bottom=0 ), min_height=10 ) elif isinstance(self.tournament, str): content = Row( Text( text=self.tournament, style=TextStyle( fill=self.session.theme.background_color, font_size=1 ), margin_top=2, margin_bottom=2, align_x=0.5 ) ) else: tournament_status_color = self.session.theme.background_color tree_button = Spacer(grow_x=False, grow_y=False) if self.tournament.status == TournamentStatus.OPEN: tournament_status_color = self.session.theme.success_color elif self.tournament.status == TournamentStatus.CLOSED: tournament_status_color = self.session.theme.danger_color elif self.tournament.status == TournamentStatus.ONGOING or self.tournament.status == TournamentStatus.COMPLETED: tournament_status_color = self.session.theme.warning_color tree_button = Button( content="Turnierbaum anzeigen", shape="rectangle", style="minor", color="hud", margin_left=4, margin_right=4, margin_top=1, on_press=self.tree_button_clicked ) ids_of_participants = [p.id for p in self.tournament.participants] color_key: Literal["hud", "danger"] = "hud" on_press_function = self.register_pressed if self.tournament.participant_type == ParticipantType.PLAYER: self.current_tournament_user_or_team_list: list[User] # IDE TypeHint participant_names = "\n".join([u.user_name for u in self.current_tournament_user_or_team_list]) if self.user and self.user.user_id in ids_of_participants: # User already registered for tournament button_text = "Abmelden" button_sensitive_hook = True # User has already accepted the rules previously color_key = "danger" on_press_function = self.unregister_pressed elif self.user and self.user.user_id not in ids_of_participants: button_text = "Anmelden" button_sensitive_hook = self.rules_accepted else: # This should NEVER happen button_text = "Anmelden" button_sensitive_hook = False elif self.tournament.participant_type == ParticipantType.TEAM: self.current_tournament_user_or_team_list: list[Team] # IDE TypeHint participant_names = "\n".join([t.name for t in self.current_tournament_user_or_team_list]) user_team_registered = [] for team in self.user_teams: if team.id in ids_of_participants: user_team_registered.append(team) if self.user and len(user_team_registered) > 0: # Any of the users teams already registered for tournament button_text = f"{user_team_registered[0].abbreviation} abmelden" button_sensitive_hook = True # User has already accepted the rules previously color_key = "danger" on_press_function = partial(self.unregister_pressed, user_team_registered[0]) elif self.user and len(user_team_registered) == 0: button_text = "Anmelden" button_sensitive_hook = self.rules_accepted else: # This should NEVER happen button_text = "Anmelden" button_sensitive_hook = False else: logger.fatal("Did someone add new values to ParticipantType ? ;)") return Column() if self.tournament.status != TournamentStatus.OPEN or self.tournament.is_full: button_sensitive_hook = False # Override button controls if tournament is not open anymore or full if self.user: accept_rules_row = Row( ThemeContextSwitcher(content=Checkbox(is_on=self.bind().rules_accepted, margin_left=4), color=self.session.theme.hud_color), Text("Ich akzeptiere die ", margin_left=1, style=TextStyle(fill=self.session.theme.background_color, font_size=0.8), overflow="nowrap", justify="right"), Link(Text("Turnierregeln", margin_right=4, style=TextStyle(fill=self.session.theme.background_color, font_size=0.8, italic=True), overflow="nowrap", justify="left"), "./tournament-rules", open_in_new_tab=True) ) button = Button( content=button_text, shape="rectangle", style="major", color=color_key, margin_left=2, margin_right=2, is_sensitive=button_sensitive_hook, on_press=on_press_function, is_loading=self.loading ) else: # No UI here if user not logged in accept_rules_row, button = Spacer(), Spacer() content = Column( Row( Image(image=from_root(f"src/ezgg_lan_manager/assets/img/games/{self.tournament.game_title.image_name}"), margin_right=1), Text( text=self.tournament.name, style=TextStyle( fill=self.session.theme.background_color, font_size=1.2 ), margin_top=1, margin_bottom=1, align_x=0.5 ), margin_right=6, margin_left=6 ), Spacer(min_height=1), TournamentDetailsInfoRow("Status", tournament_status_to_display_text(self.tournament.status), value_color=tournament_status_color), TournamentDetailsInfoRow("Startzeit", f"{weekday_to_display_text(self.tournament.start_time.weekday())}, {self.tournament.start_time.strftime('%H:%M')} Uhr"), TournamentDetailsInfoRow("Format", tournament_format_to_display_texts(self.tournament.format)[0]), TournamentDetailsInfoRow("Best of", tournament_format_to_display_texts(self.tournament.format)[1]), PointerEventListener( content=Rectangle( content=TournamentDetailsInfoRow( "Teilnehmer ▴" if self.participant_revealer_open else "Teilnehmer ▾", f"{len(self.current_tournament_user_or_team_list)} / {self.tournament.max_participants}", value_color=self.session.theme.danger_color if self.tournament.is_full else self.session.theme.background_color, key_color=self.session.theme.secondary_color ), fill=Color.TRANSPARENT, cursor="pointer" ), on_press=self.open_close_participant_revealer ), Revealer( header=None, content=Text( participant_names, style=TextStyle(fill=self.session.theme.background_color) ), is_open=self.participant_revealer_open, margin_left=4, margin_right=4 ), tree_button, Row( Text( text="Info", style=TextStyle( fill=self.session.theme.background_color, font_size=1.2 ), margin_top=1, margin_bottom=1, align_x=0.5 ) ), # FixMe: Use rio.Markdown with correct TextStyle instead to allow basic text formatting from DB-side. Text(self.tournament.description, margin_left=2, margin_right=2, style=TextStyle(fill=self.session.theme.background_color, font_size=1), overflow="wrap"), Spacer(min_height=2), accept_rules_row, Spacer(min_height=0.5), Text(self.message, margin_left=2, margin_right=2, style=TextStyle(fill=self.session.theme.success_color if self.is_success else self.session.theme.danger_color, font_size=1), overflow="wrap", justify="center"), Spacer(min_height=0.5), button ) if self.tournament and self.tournament.participant_type == ParticipantType.TEAM: content = Popup( anchor=content, content=Rectangle( content=Column( Text("Welches Team anmelden?", style=TextStyle(fill=self.session.theme.background_color, font_size=1.2), justify="center", margin_bottom=1), ThemeContextSwitcher( content=Dropdown( options=self.team_register_options, min_width=20, selected_value=self.bind().team_selected_for_register ), color=self.session.theme.hud_color, margin_bottom=1 ), Row( Button(content="Abbrechen", shape="rectangle", grow_x=False, on_press=self.on_team_register_canceled), Button(content="Anmelden", shape="rectangle", grow_x=False, on_press=self.on_team_register_confirmed), spacing=1 ), margin=0.5 ), min_width=30, min_height=4, fill=self.session.theme.primary_color, margin_top=3.5, stroke_width=0.3, stroke_color=self.session.theme.neutral_color, ), is_open=self.team_dialog_open, color="none" ) return Column( MainViewContentBox( Column( Spacer(min_height=1), content, Spacer(min_height=1) ) ), align_y=0 )