From d370f97bedd8ea005f2cbe046e66379eed913de7 Mon Sep 17 00:00:00 2001 From: Chuck Date: Wed, 1 Apr 2026 09:21:55 -0400 Subject: [PATCH 1/4] fix(masters): fix startup errors, display timing, and hole card layout Fixes three issues preventing the Masters plugin from running and improves the display experience: Startup fixes: - Replace self.plugin_dir (not set by BasePlugin) with os.path.dirname(__file__) for MastersLogoLoader initialization - Replace self.display_manager.draw_image() (doesn't exist) with self.display_manager.image.paste() matching other plugins Display timing: - Hole cards (course_tour, featured_holes) now hold each hole for hole_display_duration (default 15s) instead of switching every 1s - Paginated modes (past_champions, tournament_stats, course_overview, schedule, leaderboard) hold each page for page_display_duration (default 15s) via new _advance_page() timer - Fun facts scroll steps advance every 2s instead of every 1s - Both durations are user-configurable Visual improvements: - Countdown: Masters logo has black glow outline for visibility - Hole cards: redesigned layout with left info panel (hole #, name, par, yardage) and right side dedicated to hole image at full height - Zone badge (AMEN CORNER, FEATURED) moved to bottom-right corner to avoid overlapping left panel text Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins/masters-tournament/manager.py | 72 ++++++++++------ .../masters-tournament/masters_renderer.py | 14 +++- .../masters_renderer_enhanced.py | 84 ++++++++++++------- 3 files changed, 113 insertions(+), 57 deletions(-) diff --git a/plugins/masters-tournament/manager.py b/plugins/masters-tournament/manager.py index f4b8f72..a24e82c 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_switch = 0 + 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,18 @@ 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() + if now - self._last_hole_switch >= self._hole_switch_interval: + self._current_hole = (self._current_hole % 18) + 1 + self._last_hole_switch = 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 +422,18 @@ 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() + if now - self._last_hole_switch >= self._hole_switch_interval: + self._featured_hole_index += 1 + self._last_hole_switch = 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 +450,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 +488,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]]: diff --git a/plugins/masters-tournament/masters_renderer.py b/plugins/masters-tournament/masters_renderer.py index 2db07ae..d2a09ca 100644 --- a/plugins/masters-tournament/masters_renderer.py +++ b/plugins/masters-tournament/masters_renderer.py @@ -818,7 +818,19 @@ def render_countdown(self, days: int, hours: int, minutes: int) -> Optional[Imag ) if logo: lx = (self.width - logo.width) // 2 - img.paste(logo, (lx, 3), logo if logo.mode == "RGBA" else None) + ly = 3 + # Draw black glow behind logo for visibility + glow_pad = 2 + 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 + if logo.mode == "RGBA": + # Use alpha channel to draw black shadow + shadow = Image.new("RGBA", logo.size, (0, 0, 0, 0)) + shadow.paste((0, 0, 0), mask=logo.split()[3]) + img.paste(shadow, (lx + ox, ly + oy), shadow) + img.paste(logo, (lx, ly), logo if logo.mode == "RGBA" else None) # Countdown number - big and centered if days > 0: diff --git a/plugins/masters-tournament/masters_renderer_enhanced.py b/plugins/masters-tournament/masters_renderer_enhanced.py index 8c55791..9a83270 100644 --- a/plugins/masters-tournament/masters_renderer_enhanced.py +++ b/plugins/masters-tournament/masters_renderer_enhanced.py @@ -190,55 +190,81 @@ 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 (wrapped if needed) 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 + nw = self._text_width(draw, name_text, self.font_detail) + if nw > left_w - 4: + # Split name into words and wrap + words = name_text.split() + line1 = words[0] if words else name_text + line2 = " ".join(words[1:]) if len(words) > 1 else "" + l1w = self._text_width(draw, line1, self.font_detail) + draw.text(((left_w - l1w) // 2, name_y), line1, + fill=COLORS["masters_yellow"], font=self.font_detail) + if line2: + l2w = self._text_width(draw, line2, self.font_detail) + draw.text(((left_w - l2w) // 2, name_y + 8), line2, + fill=COLORS["masters_yellow"], font=self.font_detail) + else: + draw.text(((left_w - nw) // 2, name_y), name_text, + fill=COLORS["masters_yellow"], font=self.font_detail) + + # Par and yardage + par_y = self.height - 20 + 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 + 9), 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 From ad1a3c0ba72e9f25f145075c3b88839398b2d402 Mon Sep 17 00:00:00 2001 From: Chuck Date: Wed, 1 Apr 2026 10:10:02 -0400 Subject: [PATCH 2/4] fix(masters): per-mode hole timers, name/par overlap, and config reload - Replace shared _last_hole_switch with per-mode _last_hole_advance dict so course_tour and featured_holes don't throttle each other, and first hole holds for the full interval before advancing - Fix name/par text overlap on small displays: use width-aware line wrapping with measured heights, and anchor par/yardage below the name block with a minimum gap - Update on_config_change to refresh hole_display_duration and page_display_duration and reset timer state so hot reloads take effect Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins/masters-tournament/manager.py | 20 ++++++-- .../masters_renderer_enhanced.py | 51 ++++++++++++------- 2 files changed, 49 insertions(+), 22 deletions(-) diff --git a/plugins/masters-tournament/manager.py b/plugins/masters-tournament/manager.py index a24e82c..a1bc70f 100644 --- a/plugins/masters-tournament/manager.py +++ b/plugins/masters-tournament/manager.py @@ -112,7 +112,7 @@ def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_man self._fact_scroll = 0 # Internal timers for modes that rotate content within a display cycle - self._last_hole_switch = 0 + 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 @@ -404,9 +404,12 @@ def _display_player_cards(self, force_clear: bool) -> bool: def _display_course_tour(self, force_clear: bool) -> bool: now = time.time() - if now - self._last_hole_switch >= self._hole_switch_interval: + 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_switch = now + 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: @@ -423,9 +426,12 @@ 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() - if now - self._last_hole_switch >= self._hole_switch_interval: + 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_switch = now + self._last_hole_advance["featured"] = now + elif last == 0: + self._last_hole_advance["featured"] = now hole = featured[self._featured_hole_index % len(featured)] return self._show_image(self.renderer.render_hole_card(hole)) @@ -540,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_enhanced.py b/plugins/masters-tournament/masters_renderer_enhanced.py index 9a83270..4451899 100644 --- a/plugins/masters-tournament/masters_renderer_enhanced.py +++ b/plugins/masters-tournament/masters_renderer_enhanced.py @@ -209,28 +209,45 @@ def render_hole_card(self, hole_number: int) -> Optional[Image.Image]: self._text_shadow(draw, ((left_w - hw) // 2, 2), hole_text, self.font_header, COLORS["white"]) - # Hole name (wrapped if needed) + # Hole name — width-aware wrapping name_text = hole_info["name"] 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 > left_w - 4: - # Split name into words and wrap - words = name_text.split() - line1 = words[0] if words else name_text - line2 = " ".join(words[1:]) if len(words) > 1 else "" - l1w = self._text_width(draw, line1, self.font_detail) - draw.text(((left_w - l1w) // 2, name_y), line1, - fill=COLORS["masters_yellow"], font=self.font_detail) - if line2: - l2w = self._text_width(draw, line2, self.font_detail) - draw.text(((left_w - l2w) // 2, name_y + 8), line2, - fill=COLORS["masters_yellow"], font=self.font_detail) + if nw <= max_text_w: + name_lines = [name_text] else: - draw.text(((left_w - nw) // 2, name_y), name_text, + 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 - par_y = self.height - 20 + # 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, @@ -238,7 +255,7 @@ def render_hole_card(self, hole_number: int) -> Optional[Image.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 + 9), yard_text, + 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 ── From 48d65695ac2ded0be415232ce32e36d763f43739 Mon Sep 17 00:00:00 2001 From: Chuck Date: Wed, 1 Apr 2026 11:52:51 -0400 Subject: [PATCH 3/4] fix(masters): enlarge countdown logo and reflow layout Move the Masters logo from a small centered-top position (48x20 max) to a large left-side position (55% width, nearly full height) with black glow. Stack the countdown info (label, number, unit) vertically centered in the right panel using smaller font. The logo now dominates the display while keeping all countdown information visible. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../masters-tournament/masters_renderer.py | 64 ++++++++++++------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/plugins/masters-tournament/masters_renderer.py b/plugins/masters-tournament/masters_renderer.py index d2a09ca..420056f 100644 --- a/plugins/masters-tournament/masters_renderer.py +++ b/plugins/masters-tournament/masters_renderer.py @@ -811,28 +811,33 @@ def render_countdown(self, days: int, hours: int, minutes: int) -> Optional[Imag img = self._draw_gradient_bg(COLORS["masters_dark"], COLORS["masters_green"]) draw = ImageDraw.Draw(img) - # Masters logo centered at top + # Masters logo — large, on the left side using most of the height + logo_max_w = int(self.width * 0.55) + logo_max_h = self.height - 6 logo = self.logo_loader.get_masters_logo( - max_width=min(self.width - 10, 48), - max_height=min(self.height // 3, 20), + max_width=logo_max_w, + max_height=logo_max_h, ) + + right_x = 4 if logo: - lx = (self.width - logo.width) // 2 - ly = 3 - # Draw black glow behind logo for visibility + lx = 4 + ly = (self.height - logo.height) // 2 + # Black glow behind logo for visibility glow_pad = 2 - 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 - if logo.mode == "RGBA": - # Use alpha channel to draw black shadow - shadow = Image.new("RGBA", logo.size, (0, 0, 0, 0)) - shadow.paste((0, 0, 0), mask=logo.split()[3]) + if 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) img.paste(logo, (lx, ly), logo if logo.mode == "RGBA" else None) + right_x = lx + logo.width + 6 - # Countdown number - big and centered + # Countdown info — stacked on the right side if days > 0: count_text = str(days) unit_text = "DAYS" if days > 1 else "DAY" @@ -843,23 +848,36 @@ def render_countdown(self, days: int, hours: int, minutes: int) -> Optional[Imag count_text = "NOW" unit_text = "" - mid_y = self.height // 2 + right_w = self.width - right_x - 2 + right_cx = right_x + right_w // 2 - # "UNTIL THE MASTERS" centered + # "UNTIL THE MASTERS" above the number until_text = "UNTIL THE MASTERS" if self.tier != "tiny" else "TO MASTERS" - uw = self._text_width(draw, until_text, self.font_detail) - draw.text(((self.width - uw) // 2, mid_y - 6), + 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) + + # Vertically center the 3-line block in the right panel + 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) # 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_y = block_y + detail_h + 2 + self._text_shadow(draw, (right_cx - cw // 2, count_y), count_text, self.font_score, COLORS["masters_yellow"]) - # Unit below + # Unit below the number if unit_text: - uw2 = self._text_width(draw, unit_text, self.font_detail) - draw.text(((self.width - uw2) // 2, mid_y + 16), + 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 From deff22feeec8fd6f40e1c525a21b03e6c7d6c4b4 Mon Sep 17 00:00:00 2001 From: Chuck Date: Wed, 1 Apr 2026 13:34:40 -0400 Subject: [PATCH 4/4] fix(masters): gate split countdown layout to large displays only The two-column countdown layout (large logo left, text right) clips on tiny/small displays where the right panel is too narrow for text. Gate the split layout to tier=="large" (width > 64) with a minimum right panel width check (40px). Fall back to the compact centered layout (logo top, countdown below) for smaller displays. Also extract _draw_logo_with_glow() helper to deduplicate the black glow rendering between both layout paths. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../masters-tournament/masters_renderer.py | 118 ++++++++++-------- 1 file changed, 68 insertions(+), 50 deletions(-) diff --git a/plugins/masters-tournament/masters_renderer.py b/plugins/masters-tournament/masters_renderer.py index 420056f..6899412 100644 --- a/plugins/masters-tournament/masters_renderer.py +++ b/plugins/masters-tournament/masters_renderer.py @@ -807,37 +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 — large, on the left side using most of the height - logo_max_w = int(self.width * 0.55) - logo_max_h = self.height - 6 - logo = self.logo_loader.get_masters_logo( - max_width=logo_max_w, - max_height=logo_max_h, - ) - - right_x = 4 - if logo: - lx = 4 - ly = (self.height - logo.height) // 2 - # Black glow behind logo for visibility - glow_pad = 2 - if 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) - img.paste(logo, (lx, ly), logo if logo.mode == "RGBA" else None) - right_x = lx + logo.width + 6 - - # Countdown info — stacked on the right side + # Countdown text if days > 0: count_text = str(days) unit_text = "DAYS" if days > 1 else "DAY" @@ -848,36 +836,66 @@ def render_countdown(self, days: int, hours: int, minutes: int) -> Optional[Imag count_text = "NOW" unit_text = "" - right_w = self.width - right_x - 2 - right_cx = right_x + right_w // 2 - - # "UNTIL THE MASTERS" above the number - until_text = "UNTIL THE MASTERS" if self.tier != "tiny" else "TO 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) - - # Vertically center the 3-line block in the right panel - block_h = detail_h + 2 + count_h + 2 + detail_h - block_y = max(2, (self.height - block_h) // 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) - draw.text((right_cx - utw // 2, block_y), + 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) - count_y = block_y + detail_h + 2 - self._text_shadow(draw, (right_cx - cw // 2, count_y), + self._text_shadow(draw, ((self.width - cw) // 2, mid_y + 4), count_text, self.font_score, COLORS["masters_yellow"]) - # Unit below the number if unit_text: - uw = self._text_width(draw, unit_text, self.font_detail) - draw.text((right_cx - uw // 2, count_y + count_h + 2), + uw2 = self._text_width(draw, unit_text, self.font_detail) + draw.text(((self.width - uw2) // 2, mid_y + 16), unit_text, fill=COLORS["light_gray"], font=self.font_detail) return img