From ed80a52503c733fa54c5d4e7c859e5b86fc1eb12 Mon Sep 17 00:00:00 2001 From: Markus Walker Date: Thu, 2 Jul 2026 22:47:21 -0700 Subject: [PATCH] GUI updates to give a more modern feel in the UI/UX --- README.md | 1 + gui/gui_app.py | 636 ++++++++++++++++++++++++++++++------- tests/test_game_checker.py | 1 - tests/test_gui_app.py | 16 +- 4 files changed, 536 insertions(+), 118 deletions(-) diff --git a/README.md b/README.md index c2461f4..6a4b3d0 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ The GUI provides: - **Team Selection**: Browse and select your favorite teams - **Game Display**: View upcoming games for your selected teams + ### CLI Mode ``` python3 main.py diff --git a/gui/gui_app.py b/gui/gui_app.py index 936a6ac..d4546c0 100644 --- a/gui/gui_app.py +++ b/gui/gui_app.py @@ -5,9 +5,10 @@ from gamechecker.game_checker import game_checker from constants import SUPPORTED_LEAGUES from tkinter import ttk, messagebox -from PIL import Image, ImageTk +from PIL import Image, ImageTk, ImageDraw, ImageOps import tkinter as tk import io +import re import contextlib class FridayNightBytesGUI: @@ -16,11 +17,25 @@ def __init__(self): self.root.title("Friday Night Bytes") self.root.geometry("800x600") self.root.configure(bg="#16213e") + + self.theme = { + "bg": "#0b1220", + "surface": "#111827", + "surface_alt": "#1f2937", + "muted": "#94a3b8", + "text": "#f8fafc", + "accent": "#f59e0b", + "accent_soft": "#fbbf24", + "success": "#34d399", + "selection": "#ac9c7c" + } self.center_window() self.selected_sport = None self.selected_teams = [] + self.team_buttons = {} + self.team_cards = {} self.show_splash_screen() @@ -42,18 +57,21 @@ def clear_window(self): def show_splash_screen(self): """Display the splash screen for 5 seconds.""" self.clear_window() - - main_frame = tk.Frame(self.root, bg="#1a1a2e") + + main_frame = tk.Frame(self.root, bg=self.theme["bg"]) main_frame.pack(expand=True, fill="both") + + content_card = tk.Frame(main_frame, bg=self.theme["surface"], padx=24, pady=24) + content_card.pack(expand=True, padx=40, pady=40, fill="both") logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "logo.png") logo_img = Image.open(logo_path) - logo_img = logo_img.resize((500, 500), Image.Resampling.LANCZOS) + logo_img = logo_img.resize((340, 340), Image.Resampling.LANCZOS) self.logo_photo = ImageTk.PhotoImage(logo_img) - logo_label = tk.Label(main_frame, image=self.logo_photo, bg="#1a1a2e") - logo_label.pack(expand=True) + logo_label = tk.Label(content_card, image=self.logo_photo, bg=self.theme["surface"]) + logo_label.pack(expand=True, anchor="center") self.root.after(5000, self.show_sport_selection) @@ -61,21 +79,24 @@ def show_splash_screen(self): def show_sport_selection(self): """Display the sport selection screen with clickable sports.""" self.clear_window() - - main_frame = tk.Frame(self.root, bg="#1a1a2e") + + main_frame = tk.Frame(self.root, bg=self.theme["bg"]) main_frame.pack(expand=True, fill="both") - + + hero = tk.Frame(main_frame, bg=self.theme["bg"]) + hero.pack(fill="x", pady=(28, 8)) + title_label = tk.Label( - main_frame, - text="Choose Your Favorite Sport!", + hero, + text="Pick Your League", font=("Helvetica", 32, "bold"), - fg="#eee2dc", - bg="#1a1a2e" + fg=self.theme["text"], + bg=self.theme["bg"] ) - title_label.pack(pady=(30, 40)) - - sports_frame = tk.Frame(main_frame, bg="#1a1a2e") - sports_frame.pack(expand=True) + title_label.pack() + + sports_frame = tk.Frame(main_frame, bg=self.theme["bg"]) + sports_frame.pack(expand=True, fill="both", padx=24, pady=20) self.create_sport_button(sports_frame, "nba", 0) self.create_sport_button(sports_frame, "nfl", 1) @@ -84,7 +105,7 @@ def show_sport_selection(self): def create_sport_button(self, parent, sport_key, row): """Create a clickable sport button.""" - sport_frame = tk.Frame(parent, bg="#16213e") + sport_frame = tk.Frame(parent, bg=self.theme["surface"], bd=0, highlightthickness=0) sport_frame.grid(row=row//3, column=row%3, padx=20, pady=20, sticky="nsew") parent.grid_rowconfigure(row//3, weight=1) @@ -95,13 +116,13 @@ def create_sport_button(self, parent, sport_key, row): logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", f"{sport_key}-logo.png") logo_img = Image.open(logo_path) - logo_img = logo_img.resize((250, 250), Image.Resampling.LANCZOS) + logo_img = logo_img.resize((190, 190), Image.Resampling.LANCZOS) sport_logo = ImageTk.PhotoImage(logo_img) logo_label = tk.Label( sport_frame, image=sport_logo, - bg="#16213e", + bg=self.theme["surface"], cursor="hand2" ) @@ -110,25 +131,53 @@ def create_sport_button(self, parent, sport_key, row): self.sport_logos[sport_key] = sport_logo - logo_label.pack(expand=True) + logo_label.pack(expand=True, pady=(14, 6)) logo_label.bind("", lambda e: self.select_sport(sport_key)) + sport_name = tk.Label( + sport_frame, + text=SUPPORTED_LEAGUES[sport_key]["name"], + font=("Helvetica", 16, "bold"), + fg=self.theme["text"], + bg=self.theme["surface"] + ) + sport_name.pack(pady=(0, 4)) + sport_name.bind("", lambda e: self.select_sport(sport_key)) + + sport_hint = tk.Label( + sport_frame, + text="Select and continue", + font=("Helvetica", 10), + fg=self.theme["muted"], + bg=self.theme["surface"] + ) + sport_hint.pack(pady=(0, 16)) + sport_hint.bind("", lambda e: self.select_sport(sport_key)) + def on_enter(e): - sport_frame.config(bg="#ac9c7c") - logo_label.config(bg="#ac9c7c") + sport_frame.config(bg=self.theme["surface_alt"], highlightthickness=2, highlightbackground=self.theme["accent_soft"]) + logo_label.config(bg=self.theme["surface_alt"]) + sport_name.config(bg=self.theme["surface_alt"]) + sport_hint.config(bg=self.theme["surface_alt"]) def on_leave(e): - sport_frame.config(bg="#16213e") - logo_label.config(bg="#16213e") + sport_frame.config(bg=self.theme["surface"], highlightthickness=0) + logo_label.config(bg=self.theme["surface"]) + sport_name.config(bg=self.theme["surface"]) + sport_hint.config(bg=self.theme["surface"]) sport_frame.bind("", on_enter) sport_frame.bind("", on_leave) logo_label.bind("", on_enter) logo_label.bind("", on_leave) + sport_name.bind("", on_enter) + sport_name.bind("", on_leave) + sport_hint.bind("", on_enter) + sport_hint.bind("", on_leave) - sport_frame.config(width=400, height=300) + sport_frame.config(width=250, height=320) sport_frame.pack_propagate(False) @@ -155,36 +204,36 @@ def show_team_selection(self): return league_info = SUPPORTED_LEAGUES[self.selected_sport] - - main_frame = tk.Frame(self.root, bg="#16213e") + + main_frame = tk.Frame(self.root, bg=self.theme["bg"]) main_frame.pack(expand=True, fill="both") - - header_frame = tk.Frame(main_frame, bg="#16213e") + + header_frame = tk.Frame(main_frame, bg=self.theme["bg"]) header_frame.pack(fill="x", pady=(20, 10)) - + back_btn = tk.Button( header_frame, text="Back", font=("Helvetica", 12), - bg="#16213e", - fg="#eee2dc", - activebackground="#ac9c7c", - activeforeground="#1a1a2e", + bg=self.theme["surface"], + fg=self.theme["text"], + activebackground=self.theme["accent_soft"], + activeforeground="#111827", border=0, padx=15, pady=5, command=self.show_sport_selection ) back_btn.pack(side="left", padx=20) - + self.continue_btn = tk.Button( header_frame, text="View Games", - font=("Helvetica", 12), - bg="#16213e", - fg="#eee2dc", - activebackground="#ac9c7c", - activeforeground="#1a1a2e", + font=("Helvetica", 12, "bold"), + bg=self.theme["accent"], + fg="#111827", + activebackground=self.theme["accent_soft"], + activeforeground="#111827", border=0, padx=15, pady=5, @@ -193,16 +242,25 @@ def show_team_selection(self): title_label = tk.Label( header_frame, - text=f"Select Your Favorite Team(s)!", - font=("Helvetica", 20, "bold"), - fg="#eee2dc", - bg="#1a1a2e", + text=f"{league_info['name']} Teams", + font=("Helvetica", 24, "bold"), + fg=self.theme["text"], + bg=self.theme["bg"], ) - title_label.pack(side="left", padx=(50, 0)) + title_label.pack(side="left", padx=(34, 0)) + + helper_label = tk.Label( + main_frame, + text="Choose one or more teams to generate your game board", + font=("Helvetica", 11), + fg=self.theme["muted"], + bg=self.theme["bg"] + ) + helper_label.pack(anchor="w", padx=24, pady=(0, 10)) - canvas = tk.Canvas(main_frame, bg="#1a1a2e", highlightthickness=0) + canvas = tk.Canvas(main_frame, bg=self.theme["bg"], highlightthickness=0) scrollbar = ttk.Scrollbar(main_frame, orient="vertical", command=canvas.yview) - scrollable_frame = tk.Frame(canvas, bg="#1a1a2e") + scrollable_frame = tk.Frame(canvas, bg=self.theme["bg"]) scrollable_frame.bind( "", @@ -212,13 +270,14 @@ def show_team_selection(self): canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) - teams_wrapper = tk.Frame(scrollable_frame, bg="#1a1a2e") + teams_wrapper = tk.Frame(scrollable_frame, bg=self.theme["bg"]) teams_wrapper.pack(expand=True, fill="both", padx=20) - teams_frame = tk.Frame(teams_wrapper, bg="#1a1a2e") + teams_frame = tk.Frame(teams_wrapper, bg=self.theme["bg"]) teams_frame.pack(fill="both", expand=True, pady=20) - self.team_buttons = {} + self.team_buttons = {} + self.team_cards = {} self.teams_frame = teams_frame self.teams_wrapper = teams_wrapper @@ -279,12 +338,15 @@ def _bind_mousewheel_to_widget(self, widget): def create_team_button(self, parent, team_name, team_abbr, index): """Create a clickable team button with logo.""" + if not hasattr(self, 'team_cards'): + self.team_cards = {} + cols_per_row = getattr(self, 'cols_per_row', 4) row = index // cols_per_row col = index % cols_per_row - team_frame = tk.Frame(parent, bg="#16213e", relief="raised", bd=1) + team_frame = tk.Frame(parent, bg=self.theme["surface"], relief="flat", bd=0, padx=10, pady=10) team_frame.grid(row=row, column=col, padx=8, pady=8, sticky="nsew") # Configure grid weights for responsive behavior @@ -307,7 +369,7 @@ def create_team_button(self, parent, team_name, team_abbr, index): logo_path = potential_path break - logo_size = 80 + logo_size = 88 logo_img = Image.open(logo_path) logo_img = logo_img.resize((logo_size, logo_size), Image.Resampling.LANCZOS) team_logo = ImageTk.PhotoImage(logo_img) @@ -316,7 +378,7 @@ def create_team_button(self, parent, team_name, team_abbr, index): team_frame, image=team_logo, bg="#ffffff", - activebackground="#ac9c7c", + activebackground=self.theme["selection"], border=0, padx=15, pady=15, @@ -329,7 +391,66 @@ def create_team_button(self, parent, team_name, team_abbr, index): self.team_logos[team_abbr] = team_logo team_btn.pack(fill="both", expand=True) + + team_label = tk.Label( + team_frame, + text=team_abbr.upper(), + font=("Helvetica", 11, "bold"), + fg=self.theme["text"], + bg=self.theme["surface"] + ) + team_label.pack(pady=(10, 2)) + + team_name_label = tk.Label( + team_frame, + text=team_name, + font=("Helvetica", 9), + fg=self.theme["muted"], + bg=self.theme["surface"], + wraplength=140, + justify="center" + ) + team_name_label.pack(pady=(0, 8)) + + status_chip = tk.Label( + team_frame, + text="Tap to select", + font=("Helvetica", 8, "bold"), + fg=self.theme["muted"], + bg=self.theme["bg"], + padx=8, + pady=3 + ) + status_chip.pack() + self.team_buttons[team_abbr] = team_btn + self.team_cards[team_abbr] = { + "frame": team_frame, + "label": team_label, + "name_label": team_name_label, + "status": status_chip + } + + def on_card_enter(_event): + if team_abbr.lower() in self.selected_teams: + return + self._apply_team_card_style(team_abbr, selected=False, hovered=True) + + def on_card_leave(_event): + if team_abbr.lower() in self.selected_teams: + return + self._apply_team_card_style(team_abbr, selected=False, hovered=False) + + team_frame.bind("", on_card_enter) + team_frame.bind("", on_card_leave) + team_btn.bind("", on_card_enter) + team_btn.bind("", on_card_leave) + team_label.bind("", on_card_enter) + team_label.bind("", on_card_leave) + team_name_label.bind("", on_card_enter) + team_name_label.bind("", on_card_leave) + status_chip.bind("", on_card_enter) + status_chip.bind("", on_card_leave) if hasattr(self, '_mousewheel_callbacks'): self._bind_mousewheel_to_widget(team_btn) @@ -338,18 +459,18 @@ def create_team_button(self, parent, team_name, team_abbr, index): def toggle_team(self, team_abbr): """Toggle team selection.""" + card_meta = getattr(self, 'team_cards', {}).get(team_abbr) + if team_abbr.lower() in self.selected_teams: self.selected_teams.remove(team_abbr.lower()) self.team_buttons[team_abbr].config(bg="#ffffff") - - if hasattr(self.team_buttons[team_abbr], 'cget') and 'text' in str(self.team_buttons[team_abbr].cget('text')): - self.team_buttons[team_abbr].config(fg="#1a1a2e") + if card_meta: + self._apply_team_card_style(team_abbr, selected=False, hovered=False) else: self.selected_teams.append(team_abbr.lower()) self.team_buttons[team_abbr].config(bg="#ac9c7c") - - if hasattr(self.team_buttons[team_abbr], 'cget') and 'text' in str(self.team_buttons[team_abbr].cget('text')): - self.team_buttons[team_abbr].config(fg="#1a1a2e") + if card_meta: + self._apply_team_card_style(team_abbr, selected=True, hovered=False) # Show/hide the continue button based on team selection if self.selected_teams: @@ -360,6 +481,29 @@ def toggle_team(self, team_abbr): self.continue_btn.pack_forget() + def _apply_team_card_style(self, team_abbr, selected, hovered): + """Apply visual state to a team card based on selected/hovered status.""" + card_meta = getattr(self, 'team_cards', {}).get(team_abbr) + if not card_meta: + return + + frame_bg = self.theme["surface_alt"] if (selected or hovered) else self.theme["surface"] + label_fg = self.theme["accent_soft"] if selected else self.theme["text"] + name_fg = self.theme["text"] if selected else self.theme["muted"] + status_text = "Selected" if selected else "Tap to select" + status_fg = "#111827" if selected else self.theme["muted"] + status_bg = self.theme["accent_soft"] if selected else self.theme["bg"] + + card_meta["frame"].config( + bg=frame_bg, + highlightthickness=2 if (selected or hovered) else 0, + highlightbackground=self.theme["accent"] if selected else self.theme["accent_soft"] + ) + card_meta["label"].config(bg=frame_bg, fg=label_fg) + card_meta["name_label"].config(bg=frame_bg, fg=name_fg) + card_meta["status"].config(text=status_text, fg=status_fg, bg=status_bg) + + def show_games(self): """Display games for selected teams. You must select at least one team.""" if not self.selected_teams: @@ -377,19 +521,23 @@ def show_games(self): } self.clear_window() - - loading_frame = tk.Frame(self.root, bg="#1a1a2e") + + loading_frame = tk.Frame(self.root, bg=self.theme["bg"]) loading_frame.pack(expand=True, fill="both") - + + loading_card = tk.Frame(loading_frame, bg=self.theme["surface"], padx=30, pady=30) + loading_card.pack(expand=True, padx=40, pady=40) + loading_label = tk.Label( - loading_frame, + loading_card, text="Searching for upcoming games...", - font=("Helvetica", 20), - fg="#eee2dc", - bg="#1a1a2e" + font=("Helvetica", 22, "bold"), + fg=self.theme["text"], + bg=self.theme["surface"] ) - loading_label.pack(expand=True) - + + loading_label.pack(pady=(0, 10)) + self.root.update() try: @@ -413,73 +561,345 @@ def show_games_result(self, games_output): Or, you can just close the window. """ self.clear_window() - - main_frame = tk.Frame(self.root, bg="#1a1a2e") + + # Rich color palette for a more modern-looking results experience. + page_bg = "#0f172a" + card_bg = "#111827" + accent = "#f59e0b" + text_primary = "#f8fafc" + text_secondary = "#94a3b8" + success = "#34d399" + + game_cards = self._extract_game_cards(games_output) + + main_frame = tk.Frame(self.root, bg=page_bg) main_frame.pack(expand=True, fill="both") - - header_frame = tk.Frame(main_frame, bg="#1a1a2e") + + header_frame = tk.Frame(main_frame, bg=page_bg) header_frame.pack(fill="x", pady=(20, 10)) - + back_btn = tk.Button( header_frame, text="Back", font=("Helvetica", 12), - bg="#16213e", - fg="#eee2dc", - activebackground="#ac9c7c", - activeforeground="#1a1a2e", + bg=card_bg, + fg=text_primary, + activebackground=accent, + activeforeground="#111827", border=0, padx=15, pady=5, command=self.show_team_selection ) back_btn.pack(side="left", padx=20) - + new_search_btn = tk.Button( header_frame, text="Select New Sport", font=("Helvetica", 12), - bg="#16213e", - fg="#eee2dc", - activebackground="#ac9c7c", - activeforeground="#1a1a2e", + bg=card_bg, + fg=text_primary, + activebackground=accent, + activeforeground="#111827", border=0, padx=15, pady=5, command=self.show_sport_selection ) new_search_btn.pack(side="right", padx=20) - + title_label = tk.Label( header_frame, - text="Game Schedule", + text="Your Game Night Board", font=("Helvetica", 24, "bold"), - fg="#eee2dc", - bg="#1a1a2e" + fg=text_primary, + bg=page_bg ) title_label.pack() - - text_frame = tk.Frame(main_frame, bg="#1a1a2e") - text_frame.pack(fill="both", expand=True, padx=20, pady=20) - - text_widget = tk.Text( - text_frame, - bg="#16213e", - fg="#eee2dc", - font=("Courier", 11), - wrap="word", - padx=15, - pady=15 + + stats_frame = tk.Frame(main_frame, bg=page_bg) + stats_frame.pack(fill="x", padx=20) + + self._create_stat_chip(stats_frame, "Sport", str(self.selected_sport).upper() if self.selected_sport else "N/A", card_bg, text_primary, text_secondary) + self._create_stat_chip(stats_frame, "Teams", str(len(self.selected_teams)), card_bg, text_primary, text_secondary) + self._create_stat_chip(stats_frame, "Games Found", str(len(game_cards)), card_bg, success, text_secondary) + + content_frame = tk.Frame(main_frame, bg=page_bg) + content_frame.pack(fill="both", expand=True, padx=20, pady=(15, 20)) + + cards_frame = tk.Frame(content_frame, bg=page_bg) + cards_frame.pack(side="left", fill="both", expand=True, padx=(0, 10)) + + cards_canvas = tk.Canvas(cards_frame, bg=page_bg, highlightthickness=0) + cards_scrollbar = ttk.Scrollbar(cards_frame, orient="vertical", command=cards_canvas.yview) + cards_container = tk.Frame(cards_canvas, bg=page_bg) + + cards_container.bind( + "", + lambda e: cards_canvas.configure(scrollregion=cards_canvas.bbox("all")) ) - - scrollbar_text = ttk.Scrollbar(text_frame, orient="vertical", command=text_widget.yview) - text_widget.configure(yscrollcommand=scrollbar_text.set) - - text_widget.insert("1.0", games_output) - text_widget.config(state="disabled") - - text_widget.pack(side="left", fill="both", expand=True) - scrollbar_text.pack(side="right", fill="y") + + cards_canvas.create_window((0, 0), window=cards_container, anchor="nw") + cards_canvas.configure(yscrollcommand=cards_scrollbar.set) + + if game_cards: + for card in game_cards: + self._create_game_card(cards_container, card, card_bg, text_primary, text_secondary, accent) + else: + empty_card = tk.Frame(cards_container, bg=card_bg, padx=20, pady=20) + empty_card.pack(fill="x", pady=(0, 12)) + + empty_title = tk.Label( + empty_card, + text="No games found this week", + font=("Helvetica", 16, "bold"), + fg=text_primary, + bg=card_bg + ) + empty_title.pack(anchor="w") + + empty_body = tk.Label( + empty_card, + text="Try selecting different teams or check back tomorrow for new schedules.", + font=("Helvetica", 10), + fg=text_secondary, + bg=card_bg, + justify="left" + ) + empty_body.pack(anchor="w", pady=(8, 0)) + + cards_canvas.pack(side="left", fill="both", expand=True) + cards_scrollbar.pack(side="right", fill="y") + + + def _create_stat_chip(self, parent, label, value, bg, value_color, label_color): + """Create compact stat chips used in the results header.""" + chip = tk.Frame(parent, bg=bg, padx=12, pady=10) + chip.pack(side="left", padx=(0, 12), pady=(0, 4)) + + chip_value = tk.Label( + chip, + text=value, + font=("Helvetica", 14, "bold"), + fg=value_color, + bg=bg + ) + chip_value.pack(anchor="w") + + chip_label = tk.Label( + chip, + text=label, + font=("Helvetica", 9), + fg=label_color, + bg=bg + ) + chip_label.pack(anchor="w") + + + def _create_game_card(self, parent, card, bg, title_fg, body_fg, accent): + """Render one game card with matchup and schedule details.""" + wrapper = tk.Frame(parent, bg=bg, padx=16, pady=16) + wrapper.pack(fill="x", pady=(0, 12)) + + centered = tk.Frame(wrapper, bg=bg) + centered.pack(anchor="center") + + logos_row = tk.Frame(centered, bg=bg) + logos_row.pack(anchor="center") + + left_logo = self._get_result_team_logo(card.get("team_abbr"), card.get("team"), 52) + right_logo = self._get_result_team_logo(card.get("opponent_abbr"), card.get("opponent"), 52) + + left_logo_label = tk.Label(logos_row, image=left_logo, bg=bg) + left_logo_label.image = left_logo + left_logo_label.grid(row=0, column=0, padx=(4, 12)) + + versus = tk.Label( + logos_row, + text="VS", + font=("Helvetica", 11, "bold"), + fg=accent, + bg=bg + ) + versus.grid(row=0, column=1) + + right_logo_label = tk.Label(logos_row, image=right_logo, bg=bg) + right_logo_label.image = right_logo + right_logo_label.grid(row=0, column=2, padx=(12, 4)) + + matchup_label = tk.Label( + centered, + text=card["matchup"], + font=("Helvetica", 15, "bold"), + fg=title_fg, + bg=bg, + justify="center" + ) + matchup_label.pack(anchor="center", pady=(12, 0)) + + meta_text = f"{card['when']}" + meta_label = tk.Label( + centered, + text=meta_text, + font=("Helvetica", 10), + fg=body_fg, + bg=bg, + justify="center" + ) + meta_label.pack(anchor="center", pady=(6, 0)) + + badges = tk.Frame(centered, bg=bg) + badges.pack(anchor="center", pady=(10, 0)) + + venue_label = tk.Label( + badges, + text=card["venue_label"], + font=("Helvetica", 9, "bold"), + fg="#111827", + bg=accent, + padx=8, + pady=4 + ) + venue_label.pack(side="left", padx=(0, 8)) + + team_chip = tk.Label( + badges, + text=card.get("team_abbr", ""), + font=("Helvetica", 9, "bold"), + fg=title_fg, + bg="#1f2937", + padx=8, + pady=4 + ) + team_chip.pack(side="left") + + + def _extract_game_cards(self, games_output): + """Parse printed game table rows into structured cards for the UI.""" + cards = [] + + for raw_line in games_output.splitlines(): + line = raw_line.strip() + if not line: + continue + + # Skip separators and known non-data rows from terminal output. + if line.startswith("─") or line.startswith("Games for your favorite teams"): + continue + if line.startswith("Team") and "Matchup" in line and "Date/Time" in line: + continue + + parts = re.split(r"\s{2,}", line) + if len(parts) < 3: + continue + + team, matchup, venue_and_time = parts[0], parts[1], parts[2] + if " vs " not in matchup and " @ " not in matchup: + continue + if not venue_and_time: + continue + + venue = venue_and_time[0] + when = venue_and_time[1:].strip() if len(venue_and_time) > 1 else "TBD" + venue_label = "Home Game" if venue == "🏠" else "Away Game" + + opponent = "Unknown" + if " vs " in matchup: + opponent = matchup.split(" vs ", 1)[1].strip() + elif " @ " in matchup: + opponent = matchup.split(" @ ", 1)[1].strip() + + team_abbr = self._get_team_abbreviation_from_name(team) + opponent_abbr = self._get_team_abbreviation_from_name(opponent) + + cards.append({ + "team": team, + "matchup": matchup, + "opponent": opponent, + "team_abbr": team_abbr, + "opponent_abbr": opponent_abbr, + "when": when or "TBD", + "venue_label": venue_label + }) + + return cards + + + def _get_team_abbreviation_from_name(self, team_name): + """Get team abbreviation from full team name for the selected sport.""" + if not team_name or not self.selected_sport or self.selected_sport not in SUPPORTED_LEAGUES: + return "" + + for full_name, abbr in SUPPORTED_LEAGUES[self.selected_sport]["teams"]: + if full_name.lower() == team_name.lower(): + return abbr.upper() + + return "" + + + def _get_result_team_logo(self, team_abbr, team_name, size): + """Load and cache a team logo used in game result cards.""" + if not hasattr(self, "result_logos"): + self.result_logos = {} + + cache_key = f"{self.selected_sport}:{team_abbr}:{team_name}:{size}" + if cache_key in self.result_logos: + return self.result_logos[cache_key] + + base_dir = os.path.join( + os.path.dirname(os.path.dirname(__file__)), + "assets", + "teamlogos", + str(self.selected_sport) + ) + + candidates = [] + if team_abbr: + candidates.append(f"{team_abbr.lower()}.png") + if team_name: + candidates.append(f"{team_name.lower().replace(' ', '_')}.png") + + logo_path = None + for filename in candidates: + potential = os.path.join(base_dir, filename) + if os.path.exists(potential): + logo_path = potential + break + + if logo_path: + logo_img = Image.open(logo_path).convert("RGBA") + else: + logo_img = Image.new("RGBA", (size, size), (31, 41, 55, 255)) + + badge = self._build_logo_badge(logo_img, size) + logo_photo = ImageTk.PhotoImage(badge) + self.result_logos[cache_key] = logo_photo + return logo_photo + + + def _build_logo_badge(self, logo_img, size): + """Create a circular logo badge with a subtle glow and rim.""" + rim_color = (251, 191, 36, 255) + glow_color = (245, 158, 11, 76) + plate_color = (31, 41, 55, 255) + badge_size = size + + badge = Image.new("RGBA", (badge_size, badge_size), (0, 0, 0, 0)) + draw = ImageDraw.Draw(badge) + + draw.ellipse((1, 1, badge_size - 2, badge_size - 2), fill=glow_color) + draw.ellipse((4, 4, badge_size - 5, badge_size - 5), fill=plate_color, outline=rim_color, width=2) + + inner_size = max(12, badge_size - 12) + fitted_logo = ImageOps.fit(logo_img, (inner_size, inner_size), method=Image.Resampling.LANCZOS) + + mask = Image.new("L", (inner_size, inner_size), 0) + mask_draw = ImageDraw.Draw(mask) + mask_draw.ellipse((0, 0, inner_size - 1, inner_size - 1), fill=255) + + offset = ((badge_size - inner_size) // 2, (badge_size - inner_size) // 2) + badge.paste(fitted_logo, offset, mask) + return badge def run(self): diff --git a/tests/test_game_checker.py b/tests/test_game_checker.py index a492bb8..64c9315 100644 --- a/tests/test_game_checker.py +++ b/tests/test_game_checker.py @@ -2,7 +2,6 @@ from datetime import date import pytest - @pytest.mark.unit def test_game_display_with_nba_game(): """Test the game display functionality with a mock game.""" diff --git a/tests/test_gui_app.py b/tests/test_gui_app.py index 002224a..d5b34c5 100644 --- a/tests/test_gui_app.py +++ b/tests/test_gui_app.py @@ -198,7 +198,7 @@ def test_create_team_button_with_logo(self, mock_exists, mock_button, mock_frame self.gui.create_team_button(mock_parent, "Test Team", "TST", 0) mock_image.assert_called() - mock_img.resize.assert_called_with((80, 80), mock.ANY) + mock_img.resize.assert_called_with((88, 88), mock.ANY) @pytest.mark.unit @patch('gui.gui_app.messagebox.showwarning') @@ -261,18 +261,16 @@ def test_show_games_error(self, mock_label, mock_frame, mock_error, mock_game_ch @patch('gui.gui_app.tk.Frame') @patch('gui.gui_app.tk.Button') @patch('gui.gui_app.tk.Label') - @patch('gui.gui_app.tk.Text') + @patch('gui.gui_app.tk.Canvas') @patch('gui.gui_app.ttk.Scrollbar') - def test_show_games_result(self, mock_scrollbar, mock_text, mock_label, mock_button, mock_frame): + @patch.object(FridayNightBytesGUI, '_create_game_card') + def test_show_games_result(self, mock_create_card, mock_scrollbar, mock_canvas, mock_label, mock_button, mock_frame): """Test games result display.""" - test_output = "Test game output" + test_output = "Lakers Lakers vs Celtics 🏠 07/04/2026 08:00 PM EST" self.gui.show_games_result(test_output) - mock_text.assert_called() - - text_instance = mock_text.return_value - text_instance.insert.assert_called_with("1.0", test_output) - text_instance.config.assert_called_with(state="disabled") + mock_canvas.assert_called() + mock_create_card.assert_called_once() @pytest.mark.unit def test_run(self):