mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +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
|
# 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}")
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user