From b1295047e26c85afb8566b7bdee21b2c9fedcd54 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:34:19 -0400 Subject: [PATCH] Feature/display modes web UI (#61) * Fix leaderboard gap to use display width instead of hardcoded values - Replace hardcoded spacing (40px) with display_manager.matrix.width - Update gap calculation to use display width for blank screen simulation - Fix display width logging to show correct value - Ensures gap between league rotations matches actual display width * Add display width gap to news manager - Add display width gap at the beginning of news content - Update total_scroll_width calculation to include display width gap - Modify create_scrolling_image to draw text after display width gap - Ensures news starts with blank screen period matching display width - Removed duplicate create_scrolling_image method * add Live, Recent, Upcoming toggles to display modes on website --- src/leaderboard_manager.py | 11 +-- src/news_manager.py | 32 ++------- templates/index_v2.html | 135 ++++++++++++++++++++++++++++++++++++- 3 files changed, 147 insertions(+), 31 deletions(-) diff --git a/src/leaderboard_manager.py b/src/leaderboard_manager.py index f60ed56a..6c0aaf50 100644 --- a/src/leaderboard_manager.py +++ b/src/leaderboard_manager.py @@ -913,7 +913,8 @@ class LeaderboardManager: # Calculate total width needed total_width = 0 - spacing = 40 # Spacing between leagues + # Use display width for spacing between leagues (simulates blank screen) + spacing = self.display_manager.matrix.width # Calculate width for each league section for league_data in self.leaderboard_data: @@ -1071,12 +1072,12 @@ class LeaderboardManager: # Move to next league section (match width calculation logic) # Update current_x to where team drawing actually ended logger.info(f"League {league_idx+1} ({league_key}) teams ended at x={team_x}px") - current_x = team_x + 20 + spacing # team_x is at end of teams, add internal spacing + inter-league spacing - logger.info(f"Next league will start at x={current_x}px (gap: {20 + spacing}px)") + current_x = team_x + spacing # team_x is at end of teams, add display width gap (simulates blank screen) + logger.info(f"Next league will start at x={current_x}px (gap: {spacing}px)") # Set total scroll width for dynamic duration calculation # Use actual content width (current_x at end) instead of pre-calculated total_width - actual_content_width = current_x - (20 + spacing) # Remove the final spacing that won't be used + actual_content_width = current_x - spacing # Remove the final spacing that won't be used self.total_scroll_width = actual_content_width logger.info(f"Content width - Calculated: {total_width}px, Actual: {actual_content_width}px") @@ -1131,7 +1132,7 @@ class LeaderboardManager: else: logger.info(f" Final league ends at: {league_end_x}px") - logger.info(f"Total image width: {total_width}px, Display width: {height}px") + logger.info(f"Total image width: {total_width}px, Display width: {self.display_manager.matrix.width}px") logger.info(f"Created leaderboard image with width {total_width}") diff --git a/src/news_manager.py b/src/news_manager.py index f198083f..cc65f357 100644 --- a/src/news_manager.py +++ b/src/news_manager.py @@ -231,29 +231,6 @@ class NewsManager: self.current_headlines = display_headlines logger.debug(f"Prepared {len(display_headlines)} headlines for display") - def create_scrolling_image(self): - """Create a pre-rendered image for smooth scrolling.""" - if not self.cached_text: - self.scrolling_image = None - return - - try: - font = ImageFont.truetype(self.font_path, self.font_size) - except Exception as e: - logger.warning(f"Failed to load custom font for pre-rendering: {e}. Using default.") - font = ImageFont.load_default() - - height = self.display_manager.height - width = self.total_scroll_width - - self.scrolling_image = Image.new('RGB', (width, height), (0, 0, 0)) - draw = ImageDraw.Draw(self.scrolling_image) - - text_height = self.font_size - y_pos = (height - text_height) // 2 - draw.text((0, y_pos), self.cached_text, font=font, fill=self.text_color) - logger.debug("Pre-rendered scrolling news image created.") - def calculate_scroll_dimensions(self): """Calculate exact dimensions needed for smooth scrolling""" if not self.cached_text: @@ -274,7 +251,10 @@ class NewsManager: # Get text dimensions bbox = temp_draw.textbbox((0, 0), self.cached_text, font=font) - self.total_scroll_width = bbox[2] - bbox[0] + text_width = bbox[2] - bbox[0] + # Add display width gap at the beginning (simulates blank screen) + display_width = self.display_manager.width + self.total_scroll_width = display_width + text_width # Calculate dynamic display duration self.calculate_dynamic_duration() @@ -307,7 +287,9 @@ class NewsManager: text_height = self.font_size y_pos = (height - text_height) // 2 - draw.text((0, y_pos), self.cached_text, font=font, fill=self.text_color) + # Draw text starting after display width gap (simulates blank screen) + display_width = self.display_manager.width + draw.text((display_width, y_pos), self.cached_text, font=font, fill=self.text_color) logger.debug("Pre-rendered scrolling news image created.") def calculate_dynamic_duration(self): diff --git a/templates/index_v2.html b/templates/index_v2.html index 65a88c59..166303a2 100644 --- a/templates/index_v2.html +++ b/templates/index_v2.html @@ -90,6 +90,56 @@ color: var(--warning-color); } + /* Display Mode Toggle Styles */ + .display-mode-toggle { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border: 2px solid #dee2e6; + border-radius: 8px; + padding: 12px; + margin: 8px 0; + transition: all 0.3s ease; + } + + .display-mode-toggle:hover { + border-color: #6c757d; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + } + + .display-mode-toggle label { + display: flex; + align-items: center; + gap: 10px; + margin: 0; + cursor: pointer; + font-weight: 500; + transition: color 0.2s ease; + } + + .display-mode-toggle label:hover { + color: var(--primary-color); + } + + .display-mode-toggle input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--secondary-color); + } + + .display-mode-toggle .mode-icon { + font-size: 16px; + width: 20px; + text-align: center; + } + + .display-mode-toggle .mode-label { + font-size: 14px; + font-weight: 600; + } + + .mode-live { color: #e74c3c; } + .mode-recent { color: #f39c12; } + .mode-upcoming { color: #3498db; } + .main-grid { display: grid; grid-template-columns: 1fr; @@ -3508,6 +3558,10 @@ const liveUpd = sec.live_update_interval ?? 30; const recentUpd = sec.recent_update_interval ?? 3600; const upcomingUpd = sec.upcoming_update_interval ?? 3600; + const displayModes = sec.display_modes || {}; + const liveModeEnabled = displayModes[`${p}_live`] ?? true; + const recentModeEnabled = displayModes[`${p}_recent`] ?? true; + const upcomingModeEnabled = displayModes[`${p}_upcoming`] ?? true; return `
@@ -3522,6 +3576,32 @@
+
+

+ Display Modes +

+
+ +
+
+ +
+
+ +
+
@@ -3569,6 +3649,36 @@ `; }).join(''); container.innerHTML = html || 'No sports configuration found.'; + + // Add event listeners for display mode toggles + const displayModeCheckboxes = container.querySelectorAll('.sp-display-mode'); + displayModeCheckboxes.forEach(checkbox => { + checkbox.addEventListener('change', function() { + const league = this.getAttribute('data-league'); + const mode = this.getAttribute('data-mode'); + const isEnabled = this.checked; + + // Visual feedback + const label = this.closest('label'); + const toggle = this.closest('.display-mode-toggle'); + + if (isEnabled) { + toggle.style.backgroundColor = 'rgba(46, 204, 113, 0.1)'; + toggle.style.borderColor = '#2ecc71'; + } else { + toggle.style.backgroundColor = 'rgba(231, 76, 60, 0.1)'; + toggle.style.borderColor = '#e74c3c'; + } + + // Reset after a short delay + setTimeout(() => { + toggle.style.backgroundColor = ''; + toggle.style.borderColor = ''; + }, 1000); + + showNotification(`${league.toUpperCase()} ${mode} mode ${isEnabled ? 'enabled' : 'disabled'}`, 'success'); + }); + }); } catch (err) { document.getElementById('sports-config').textContent = 'Failed to load sports configuration'; } @@ -3591,6 +3701,24 @@ const upcomingUpd = parseInt(document.querySelector(`.sp-upcoming-update[data-league="${key}"]`)?.value || '3600'); const recentCount = parseInt(document.querySelector(`.sp-recent-count[data-league="${key}"]`)?.value || '1'); const upcomingCount = parseInt(document.querySelector(`.sp-upcoming-count[data-league="${key}"]`)?.value || '1'); + + // Get display modes + const leaguePrefixes = { + 'nfl_scoreboard': 'nfl', + 'mlb': 'mlb', + 'milb': 'milb', + 'nhl_scoreboard': 'nhl', + 'nba_scoreboard': 'nba', + 'ncaa_fb_scoreboard': 'ncaa_fb', + 'ncaa_baseball_scoreboard': 'ncaa_baseball', + 'ncaam_basketball_scoreboard': 'ncaam_basketball', + 'soccer_scoreboard': 'soccer' + }; + const p = leaguePrefixes[key] || key; + const liveModeEnabled = document.querySelector(`.sp-display-mode[data-league="${key}"][data-mode="live"]`)?.checked || false; + const recentModeEnabled = document.querySelector(`.sp-display-mode[data-league="${key}"][data-mode="recent"]`)?.checked || false; + const upcomingModeEnabled = document.querySelector(`.sp-display-mode[data-league="${key}"][data-mode="upcoming"]`)?.checked || false; + fragment[key] = { enabled, live_priority: livePriority, @@ -3601,7 +3729,12 @@ recent_update_interval: recentUpd, upcoming_update_interval: upcomingUpd, recent_games_to_show: recentCount, - upcoming_games_to_show: upcomingCount + upcoming_games_to_show: upcomingCount, + display_modes: { + [`${p}_live`]: liveModeEnabled, + [`${p}_recent`]: recentModeEnabled, + [`${p}_upcoming`]: upcomingModeEnabled + } }; }); await saveConfigJson(fragment);