diff --git a/plugins/masters-tournament/manager.py b/plugins/masters-tournament/manager.py index f4b8f72..a1bc70f 100644 --- a/plugins/masters-tournament/manager.py +++ b/plugins/masters-tournament/manager.py @@ -7,6 +7,7 @@ """ import logging +import os import time from datetime import datetime from typing import Any, Dict, List, Optional @@ -54,7 +55,7 @@ def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_man self.display_duration = config.get("display_duration", 20) # Initialize components - self.logo_loader = MastersLogoLoader(self.plugin_dir) + self.logo_loader = MastersLogoLoader(os.path.dirname(os.path.abspath(__file__))) self.data_source = MastersDataSource(cache_manager, config) # Use enhanced renderer for 64x32+, base for tiny displays @@ -110,6 +111,14 @@ def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_man self._fact_index = 0 self._fact_scroll = 0 + # Internal timers for modes that rotate content within a display cycle + self._last_hole_advance = {} # per-mode hole timers + self._hole_switch_interval = config.get("hole_display_duration", 15) + self._last_fact_advance = 0 + self._fact_advance_interval = 2 # seconds between scroll steps + self._last_page_advance = {} # per-mode page timers + self._page_interval = config.get("page_display_duration", 15) + # Player card rotation self._player_card_index = 0 @@ -357,10 +366,21 @@ def display(self, force_clear: bool = False, display_mode: Optional[str] = None) self.logger.warning(f"Unknown display mode: {display_mode}") return False + def _advance_page(self, key: str) -> int: + """Return current page for a mode, advancing only after page_interval seconds.""" + now = time.time() + last = self._last_page_advance.get(key, 0) + if last > 0 and now - last >= self._page_interval: + self._page[key] = self._page.get(key, 0) + 1 + self._last_page_advance[key] = now + elif last == 0: + self._last_page_advance[key] = now + return self._page.get(key, 0) + def _show_image(self, image: Optional[Image.Image]) -> bool: """Helper to display an image if it exists.""" if image: - self.display_manager.draw_image(image, 0, 0) + self.display_manager.image.paste(image, (0, 0)) self.display_manager.update_display() return True return False @@ -368,12 +388,10 @@ def _show_image(self, image: Optional[Image.Image]) -> bool: def _display_leaderboard(self, force_clear: bool) -> bool: if not self._leaderboard_data: return False - page = self._page["leaderboard"] - result = self._show_image( + page = self._advance_page("leaderboard") + return self._show_image( self.renderer.render_leaderboard(self._leaderboard_data, show_favorites=True, page=page) ) - self._page["leaderboard"] = page + 1 - return result def _display_player_cards(self, force_clear: bool) -> bool: if not self._leaderboard_data: @@ -385,18 +403,21 @@ def _display_player_cards(self, force_clear: bool) -> bool: return self._show_image(self.renderer.render_player_card(player)) def _display_course_tour(self, force_clear: bool) -> bool: - result = self._show_image(self.renderer.render_hole_card(self._current_hole)) - self._current_hole = (self._current_hole % 18) + 1 - return result + now = time.time() + last = self._last_hole_advance.get("course_tour", 0) + if last > 0 and now - last >= self._hole_switch_interval: + self._current_hole = (self._current_hole % 18) + 1 + self._last_hole_advance["course_tour"] = now + elif last == 0: + self._last_hole_advance["course_tour"] = now + return self._show_image(self.renderer.render_hole_card(self._current_hole)) def _display_amen_corner(self, force_clear: bool) -> bool: return self._show_image(self.renderer.render_amen_corner()) def _display_past_champions(self, force_clear: bool) -> bool: - page = self._page["champions"] - result = self._show_image(self.renderer.render_past_champions(page=page)) - self._page["champions"] = page + 1 - return result + page = self._advance_page("champions") + return self._show_image(self.renderer.render_past_champions(page=page)) def _display_hole_by_hole(self, force_clear: bool) -> bool: """Display hole-by-hole course tour (same as course_tour).""" @@ -404,17 +425,21 @@ def _display_hole_by_hole(self, force_clear: bool) -> bool: def _display_featured_holes(self, force_clear: bool) -> bool: featured = [12, 13, 15, 16] + now = time.time() + last = self._last_hole_advance.get("featured", 0) + if last > 0 and now - last >= self._hole_switch_interval: + self._featured_hole_index += 1 + self._last_hole_advance["featured"] = now + elif last == 0: + self._last_hole_advance["featured"] = now hole = featured[self._featured_hole_index % len(featured)] - self._featured_hole_index += 1 return self._show_image(self.renderer.render_hole_card(hole)) def _display_schedule(self, force_clear: bool) -> bool: - page = self._page["schedule"] - result = self._show_image( + page = self._advance_page("schedule") + return self._show_image( self.renderer.render_schedule(self._schedule_data, page=page) ) - self._page["schedule"] = page + 1 - return result def _display_live_action(self, force_clear: bool) -> bool: """Show live alert if enhanced renderer available, else leaderboard.""" @@ -431,16 +456,17 @@ def _display_live_action(self, force_clear: bool) -> bool: return self._display_leaderboard(force_clear) def _display_tournament_stats(self, force_clear: bool) -> bool: - page = self._page["stats"] - result = self._show_image(self.renderer.render_tournament_stats(page=page)) - self._page["stats"] = page + 1 - return result + page = self._advance_page("stats") + return self._show_image(self.renderer.render_tournament_stats(page=page)) def _display_fun_facts(self, force_clear: bool) -> bool: result = self._show_image( self.renderer.render_fun_fact(self._fact_index, scroll_offset=self._fact_scroll) ) - self._fact_scroll += 1 + now = time.time() + if now - self._last_fact_advance >= self._fact_advance_interval: + self._fact_scroll += 1 + self._last_fact_advance = now # Move to next fact after scrolling through if self._fact_scroll > 5: self._fact_index += 1 @@ -468,10 +494,8 @@ def _display_field_overview(self, force_clear: bool) -> bool: def _display_course_overview(self, force_clear: bool) -> bool: if hasattr(self.renderer, "render_course_overview"): - page = self._page["course_overview"] - result = self._show_image(self.renderer.render_course_overview(page=page)) - self._page["course_overview"] = page + 1 - return result + page = self._advance_page("course_overview") + return self._show_image(self.renderer.render_course_overview(page=page)) return self._display_amen_corner(force_clear) def get_vegas_content(self) -> Optional[List[Image.Image]]: @@ -522,6 +546,10 @@ def on_config_change(self, new_config): super().on_config_change(new_config) self._update_interval = new_config.get("update_interval", 30) self.display_duration = new_config.get("display_duration", 20) + self._hole_switch_interval = new_config.get("hole_display_duration", 15) + self._page_interval = new_config.get("page_display_duration", 15) + self._last_hole_advance.clear() + self._last_page_advance.clear() self.modes = self._build_enabled_modes() self._last_update = 0 diff --git a/plugins/masters-tournament/masters_renderer.py b/plugins/masters-tournament/masters_renderer.py index 2db07ae..6899412 100644 --- a/plugins/masters-tournament/masters_renderer.py +++ b/plugins/masters-tournament/masters_renderer.py @@ -807,20 +807,25 @@ def render_schedule(self, schedule_data: List[Dict], page: int = 0) -> Optional[ # COUNTDOWN - Centered and spacious # ═══════════════════════════════════════════════════════════ + def _draw_logo_with_glow(self, img, logo, lx, ly, glow_pad=2): + """Paste a logo with a black glow outline for visibility.""" + if logo and logo.mode == "RGBA": + alpha = logo.split()[3] + shadow = Image.new("RGBA", logo.size, (0, 0, 0, 0)) + shadow.paste((0, 0, 0), mask=alpha) + for ox in range(-glow_pad, glow_pad + 1): + for oy in range(-glow_pad, glow_pad + 1): + if ox == 0 and oy == 0: + continue + img.paste(shadow, (lx + ox, ly + oy), shadow) + if logo: + img.paste(logo, (lx, ly), logo if logo.mode == "RGBA" else None) + def render_countdown(self, days: int, hours: int, minutes: int) -> Optional[Image.Image]: img = self._draw_gradient_bg(COLORS["masters_dark"], COLORS["masters_green"]) draw = ImageDraw.Draw(img) - # Masters logo centered at top - logo = self.logo_loader.get_masters_logo( - max_width=min(self.width - 10, 48), - max_height=min(self.height // 3, 20), - ) - if logo: - lx = (self.width - logo.width) // 2 - img.paste(logo, (lx, 3), logo if logo.mode == "RGBA" else None) - - # Countdown number - big and centered + # Countdown text if days > 0: count_text = str(days) unit_text = "DAYS" if days > 1 else "DAY" @@ -831,20 +836,63 @@ def render_countdown(self, days: int, hours: int, minutes: int) -> Optional[Imag count_text = "NOW" unit_text = "" - mid_y = self.height // 2 + # Two-column layout only on large displays (width > 64) + min_right_width = 40 + if self.tier == "large": + logo = self.logo_loader.get_masters_logo( + max_width=int(self.width * 0.55), + max_height=self.height - 6, + ) + if logo and (self.width - logo.width - 12) >= min_right_width: + lx = 4 + ly = (self.height - logo.height) // 2 + self._draw_logo_with_glow(img, logo, lx, ly) + right_x = lx + logo.width + 6 + right_w = self.width - right_x - 2 + right_cx = right_x + right_w // 2 + + until_text = "UNTIL THE MASTERS" + utw = self._text_width(draw, until_text, self.font_detail) + if utw > right_w: + until_text = "TO MASTERS" + utw = self._text_width(draw, until_text, self.font_detail) + + detail_h = self._text_height(draw, "A", self.font_detail) + count_h = self._text_height(draw, count_text, self.font_score) + block_h = detail_h + 2 + count_h + 2 + detail_h + block_y = max(2, (self.height - block_h) // 2) + + draw.text((right_cx - utw // 2, block_y), + until_text, fill=COLORS["white"], font=self.font_detail) + cw = self._text_width(draw, count_text, self.font_score) + count_y = block_y + detail_h + 2 + self._text_shadow(draw, (right_cx - cw // 2, count_y), + count_text, self.font_score, COLORS["masters_yellow"]) + if unit_text: + uw = self._text_width(draw, unit_text, self.font_detail) + draw.text((right_cx - uw // 2, count_y + count_h + 2), + unit_text, fill=COLORS["light_gray"], font=self.font_detail) + return img + + # Compact layout: logo centered at top, countdown below + logo = self.logo_loader.get_masters_logo( + max_width=min(self.width - 10, 48), + max_height=min(self.height // 3, 20), + ) + if logo: + lx = (self.width - logo.width) // 2 + self._draw_logo_with_glow(img, logo, lx, 3) - # "UNTIL THE MASTERS" centered - until_text = "UNTIL THE MASTERS" if self.tier != "tiny" else "TO MASTERS" + mid_y = self.height // 2 + until_text = "TO MASTERS" if self.tier == "tiny" else "UNTIL THE MASTERS" uw = self._text_width(draw, until_text, self.font_detail) draw.text(((self.width - uw) // 2, mid_y - 6), until_text, fill=COLORS["white"], font=self.font_detail) - # Big countdown number cw = self._text_width(draw, count_text, self.font_score) self._text_shadow(draw, ((self.width - cw) // 2, mid_y + 4), count_text, self.font_score, COLORS["masters_yellow"]) - # Unit below if unit_text: uw2 = self._text_width(draw, unit_text, self.font_detail) draw.text(((self.width - uw2) // 2, mid_y + 16), diff --git a/plugins/masters-tournament/masters_renderer_enhanced.py b/plugins/masters-tournament/masters_renderer_enhanced.py index 8c55791..4451899 100644 --- a/plugins/masters-tournament/masters_renderer_enhanced.py +++ b/plugins/masters-tournament/masters_renderer_enhanced.py @@ -190,55 +190,98 @@ def render_player_card(self, player: Dict) -> Optional[Image.Image]: return img def render_hole_card(self, hole_number: int) -> Optional[Image.Image]: - """Enhanced hole card.""" + """Enhanced hole card — left info panel, right hole image using full height.""" hole_info = get_hole_info(hole_number) img = self._draw_gradient_bg((10, 70, 25), COLORS["augusta_green"]) draw = ImageDraw.Draw(img) - # Header - h = self.header_height - draw.rectangle([(0, 0), (self.width - 1, h - 1)], fill=COLORS["masters_green"]) - draw.line([(0, h - 1), (self.width, h - 1)], fill=COLORS["masters_yellow"]) + # Left panel width for text info + left_w = 38 if self.tier == "large" else 28 + # ── Left panel: hole info ── + draw.rectangle([(0, 0), (left_w - 1, self.height - 1)], fill=COLORS["masters_dark"]) + draw.line([(left_w - 1, 0), (left_w - 1, self.height)], fill=COLORS["masters_yellow"]) + + # Hole number hole_text = f"#{hole_number}" - self._text_shadow(draw, (3, 1), hole_text, self.font_header, COLORS["white"]) + hw = self._text_width(draw, hole_text, self.font_header) + self._text_shadow(draw, ((left_w - hw) // 2, 2), hole_text, + self.font_header, COLORS["white"]) + # Hole name — width-aware wrapping name_text = hole_info["name"] - nw = self._text_width(draw, name_text, self.font_body) - draw.text((self.width - nw - 3, 1), name_text, - fill=COLORS["masters_yellow"], font=self.font_body) + name_y = 12 if self.tier == "tiny" else 14 + line_h = self._text_height(draw, "A", self.font_detail) + 1 + max_text_w = left_w - 4 + + name_lines = [] + nw = self._text_width(draw, name_text, self.font_detail) + if nw <= max_text_w: + name_lines = [name_text] + else: + words = name_text.split() + current = "" + for word in words: + test = f"{current} {word}".strip() if current else word + if self._text_width(draw, test, self.font_detail) <= max_text_w: + current = test + else: + if current: + name_lines.append(current) + current = word + if current: + # Truncate last line with ellipsis if too wide + if self._text_width(draw, current, self.font_detail) > max_text_w: + while len(current) > 1 and self._text_width(draw, current + "..", self.font_detail) > max_text_w: + current = current[:-1] + current = current + ".." + name_lines.append(current) + + for i, line in enumerate(name_lines): + lw = self._text_width(draw, line, self.font_detail) + draw.text(((left_w - lw) // 2, name_y + i * line_h), line, + fill=COLORS["masters_yellow"], font=self.font_detail) + + # Par and yardage — anchored to bottom, above name block + name_block_bottom = name_y + len(name_lines) * line_h + par_yard_h = line_h * 2 + 2 # two lines plus padding + par_y = max(name_block_bottom + 2, self.height - par_yard_h - 2) + + par_text = f"Par {hole_info['par']}" + pw = self._text_width(draw, par_text, self.font_detail) + draw.text(((left_w - pw) // 2, par_y), par_text, + fill=COLORS["white"], font=self.font_detail) - # Hole layout image + yard_text = f"{hole_info['yardage']}y" + yw = self._text_width(draw, yard_text, self.font_detail) + draw.text(((left_w - yw) // 2, par_y + line_h), yard_text, + fill=COLORS["light_gray"], font=self.font_detail) + + # ── Right side: hole layout image using full height ── + img_x = left_w + 2 + img_w = self.width - img_x - 2 + img_h = self.height - 4 hole_img = self.logo_loader.get_hole_image( hole_number, - max_width=self.width - 6, - max_height=self.height - h - 14, + max_width=img_w, + max_height=img_h, ) if hole_img: - hx = (self.width - hole_img.width) // 2 - hy = h + 1 + hx = img_x + (img_w - hole_img.width) // 2 + hy = (self.height - hole_img.height) // 2 img.paste(hole_img, (hx, hy), hole_img if hole_img.mode == "RGBA" else None) - # Footer bar - fy = self.height - 10 - draw.rectangle([(0, fy), (self.width - 1, self.height - 1)], fill=(0, 0, 0)) - draw.line([(0, fy), (self.width, fy)], fill=COLORS["masters_yellow"]) - - info_text = f"Par {hole_info['par']} {hole_info['yardage']} yards" - iw = self._text_width(draw, info_text, self.font_detail) - draw.text(((self.width - iw) // 2, fy + 2), info_text, - fill=COLORS["white"], font=self.font_detail) - + # Zone badge at bottom-right corner (over the hole image area) zone = hole_info.get("zone") if zone and self.tier != "tiny": badge = zone.upper() bw = self._text_width(draw, badge, self.font_detail) + 4 - draw.rectangle( - [(self.width - bw - 1, fy + 1), (self.width - 2, self.height - 2)], - fill=COLORS["masters_dark"], - ) - draw.text((self.width - bw + 1, fy + 2), badge, + bx = self.width - bw - 1 + by = self.height - 9 + draw.rectangle([(bx, by), (self.width - 1, self.height - 1)], + fill=COLORS["masters_dark"]) + draw.text((bx + 2, by + 1), badge, fill=COLORS["masters_yellow"], font=self.font_detail) return img