Add Tournaments UI #32
@ -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,
|
||||
))
|
||||
|
||||
@ -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
|
||||
|
||||
BIN
src/ezgg_lan_manager/assets/img/games/rl.png
Normal file
BIN
src/ezgg_lan_manager/assets/img/games/rl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
src/ezgg_lan_manager/assets/img/games/teeworlds.png
Normal file
BIN
src/ezgg_lan_manager/assets/img/games/teeworlds.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
BIN
src/ezgg_lan_manager/assets/img/games/worms.png
Normal file
BIN
src/ezgg_lan_manager/assets/img/games/worms.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
60
src/ezgg_lan_manager/components/TournamentPageRow.py
Normal file
60
src/ezgg_lan_manager/components/TournamentPageRow.py
Normal 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
|
||||
)
|
||||
65
src/ezgg_lan_manager/pages/TournamentDetailsPage.py
Normal file
65
src/ezgg_lan_manager/pages/TournamentDetailsPage.py
Normal 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
|
||||
)
|
||||
@ -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
|
||||
|
||||
@ -20,3 +20,4 @@ from .ManageUsersPage import ManageUsersPage
|
||||
from .ManageCateringPage import ManageCateringPage
|
||||
from .ManageTournamentsPage import ManageTournamentsPage
|
||||
from .OverviewPage import OverviewPage
|
||||
from .TournamentDetailsPage import TournamentDetailsPage
|
||||
|
||||
86
src/ezgg_lan_manager/services/TournamentService.py
Normal file
86
src/ezgg_lan_manager/services/TournamentService.py
Normal 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
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user