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
This commit is contained in:
Chuck
2025-09-24 13:34:19 -04:00
committed by GitHub
parent 7c18b5126e
commit b1295047e2
3 changed files with 147 additions and 31 deletions

View File

@@ -913,7 +913,8 @@ class LeaderboardManager:
# Calculate total width needed # Calculate total width needed
total_width = 0 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 # Calculate width for each league section
for league_data in self.leaderboard_data: for league_data in self.leaderboard_data:
@@ -1071,12 +1072,12 @@ class LeaderboardManager:
# Move to next league section (match width calculation logic) # Move to next league section (match width calculation logic)
# Update current_x to where team drawing actually ended # 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") 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 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: {20 + spacing}px)") logger.info(f"Next league will start at x={current_x}px (gap: {spacing}px)")
# Set total scroll width for dynamic duration calculation # Set total scroll width for dynamic duration calculation
# Use actual content width (current_x at end) instead of pre-calculated total_width # 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 self.total_scroll_width = actual_content_width
logger.info(f"Content width - Calculated: {total_width}px, Actual: {actual_content_width}px") logger.info(f"Content width - Calculated: {total_width}px, Actual: {actual_content_width}px")
@@ -1131,7 +1132,7 @@ class LeaderboardManager:
else: else:
logger.info(f" Final league ends at: {league_end_x}px") 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}") logger.info(f"Created leaderboard image with width {total_width}")

View File

@@ -231,29 +231,6 @@ class NewsManager:
self.current_headlines = display_headlines self.current_headlines = display_headlines
logger.debug(f"Prepared {len(display_headlines)} headlines for display") 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): def calculate_scroll_dimensions(self):
"""Calculate exact dimensions needed for smooth scrolling""" """Calculate exact dimensions needed for smooth scrolling"""
if not self.cached_text: if not self.cached_text:
@@ -274,7 +251,10 @@ class NewsManager:
# Get text dimensions # Get text dimensions
bbox = temp_draw.textbbox((0, 0), self.cached_text, font=font) 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 # Calculate dynamic display duration
self.calculate_dynamic_duration() self.calculate_dynamic_duration()
@@ -307,7 +287,9 @@ class NewsManager:
text_height = self.font_size text_height = self.font_size
y_pos = (height - text_height) // 2 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.") logger.debug("Pre-rendered scrolling news image created.")
def calculate_dynamic_duration(self): def calculate_dynamic_duration(self):

View File

@@ -90,6 +90,56 @@
color: var(--warning-color); 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 { .main-grid {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -3508,6 +3558,10 @@
const liveUpd = sec.live_update_interval ?? 30; const liveUpd = sec.live_update_interval ?? 30;
const recentUpd = sec.recent_update_interval ?? 3600; const recentUpd = sec.recent_update_interval ?? 3600;
const upcomingUpd = sec.upcoming_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 ` return `
<div style="border:1px solid #ddd; border-radius:6px; padding:12px; margin:10px 0;"> <div style="border:1px solid #ddd; border-radius:6px; padding:12px; margin:10px 0;">
<div style="display:flex; justify-content: space-between; align-items:center; margin-bottom:8px;"> <div style="display:flex; justify-content: space-between; align-items:center; margin-bottom:8px;">
@@ -3522,6 +3576,32 @@
<button type="button" class="btn btn-secondary" onclick="stopOnDemand()"><i class="fas fa-ban"></i> Stop</button> <button type="button" class="btn btn-secondary" onclick="stopOnDemand()"><i class="fas fa-ban"></i> Stop</button>
</div> </div>
</div> </div>
<div style="margin-top:15px;">
<h4 style="margin: 0 0 10px 0; color: var(--primary-color); font-size: 16px;">
<i class="fas fa-toggle-on"></i> Display Modes
</h4>
<div class="display-mode-toggle">
<label>
<input type="checkbox" data-league="${l.key}" class="sp-display-mode" data-mode="live" ${liveModeEnabled ? 'checked' : ''}>
<i class="fas fa-circle mode-icon mode-live"></i>
<span class="mode-label mode-live">Live Mode</span>
</label>
</div>
<div class="display-mode-toggle">
<label>
<input type="checkbox" data-league="${l.key}" class="sp-display-mode" data-mode="recent" ${recentModeEnabled ? 'checked' : ''}>
<i class="fas fa-history mode-icon mode-recent"></i>
<span class="mode-label mode-recent">Recent Mode</span>
</label>
</div>
<div class="display-mode-toggle">
<label>
<input type="checkbox" data-league="${l.key}" class="sp-display-mode" data-mode="upcoming" ${upcomingModeEnabled ? 'checked' : ''}>
<i class="fas fa-clock mode-icon mode-upcoming"></i>
<span class="mode-label mode-upcoming">Upcoming Mode</span>
</label>
</div>
</div>
<div class="form-row" style="margin-top:10px;"> <div class="form-row" style="margin-top:10px;">
<div class="form-group"> <div class="form-group">
<label>Live Priority</label> <label>Live Priority</label>
@@ -3569,6 +3649,36 @@
`; `;
}).join(''); }).join('');
container.innerHTML = html || 'No sports configuration found.'; 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) { } catch (err) {
document.getElementById('sports-config').textContent = 'Failed to load sports configuration'; 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 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 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'); 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] = { fragment[key] = {
enabled, enabled,
live_priority: livePriority, live_priority: livePriority,
@@ -3601,7 +3729,12 @@
recent_update_interval: recentUpd, recent_update_interval: recentUpd,
upcoming_update_interval: upcomingUpd, upcoming_update_interval: upcomingUpd,
recent_games_to_show: recentCount, 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); await saveConfigJson(fragment);