mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
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:
@@ -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}")
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 `
|
||||
<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;">
|
||||
@@ -3522,6 +3576,32 @@
|
||||
<button type="button" class="btn btn-secondary" onclick="stopOnDemand()"><i class="fas fa-ban"></i> Stop</button>
|
||||
</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-group">
|
||||
<label>Live Priority</label>
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user