mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
* Fix NCAAFB ranking display issue - Remove duplicate ranking system that was drawing rankings behind team logos - Old system (_get_rank) was drawing rankings at top of logos - New system (_fetch_team_rankings) correctly draws rankings in bottom corners - Remove old ranking calls from live, recent, and upcoming game drawing functions - Remove unnecessary _fetch_rankings() calls from update methods - Rankings now only appear in designated corner positions, not overlapping logos Fixes issue where team rankings/betting lines were being drawn behind team logos instead of replacing team records in the corners. * Add missing show_ranking and show_records options to NCAAFB web UI - Add show_ranking option to NCAAFB scoreboard config template - Add show_records and show_ranking toggle switches to NCAAFB web UI - Update JavaScript form collection to include new fields - Users can now control whether to show team records or rankings via web interface This completes the fix for NCAAFB ranking display - users can now enable show_ranking in the web UI to see AP Top 25 rankings instead of team records.
3996 lines
222 KiB
HTML
3996 lines
222 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>LED Matrix Config</title>
|
|
<style>
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
margin: 20px;
|
|
background-color: #f4f4f4;
|
|
color: #333;
|
|
}
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: auto;
|
|
background: #fff;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 0 15px rgba(0,0,0,0.1);
|
|
}
|
|
h1, h2, h3 {
|
|
text-align: center;
|
|
color: #333;
|
|
margin-bottom: 20px;
|
|
}
|
|
.tabs {
|
|
display: flex;
|
|
border-bottom: 1px solid #ccc;
|
|
margin-bottom: 20px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.tab-link {
|
|
padding: 12px 20px;
|
|
cursor: pointer;
|
|
border: none;
|
|
background-color: transparent;
|
|
font-size: 14px;
|
|
border-bottom: 3px solid transparent;
|
|
transition: all 0.3s;
|
|
white-space: nowrap;
|
|
}
|
|
.tab-link.active {
|
|
border-bottom: 3px solid #4CAF50;
|
|
font-weight: bold;
|
|
background-color: #f0f8f0;
|
|
}
|
|
.tab-content {
|
|
display: none;
|
|
padding: 20px 0;
|
|
}
|
|
.checkbox-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: 10px;
|
|
margin: 10px 0;
|
|
}
|
|
.checkbox-item {
|
|
padding: 8px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
background-color: #f9f9f9;
|
|
}
|
|
.checkbox-item label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin: 0;
|
|
cursor: pointer;
|
|
}
|
|
.custom-feeds-container {
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
padding: 15px;
|
|
background-color: #f9f9f9;
|
|
}
|
|
.add-custom-feed {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 15px;
|
|
align-items: center;
|
|
}
|
|
.custom-feed-item {
|
|
margin: 10px 0;
|
|
padding: 10px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
background-color: white;
|
|
}
|
|
.custom-feed-info {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.remove-btn {
|
|
background-color: #ff4444;
|
|
color: white;
|
|
border: none;
|
|
padding: 4px 8px;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
}
|
|
.remove-btn:hover {
|
|
background-color: #cc0000;
|
|
}
|
|
.settings-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
}
|
|
.status-container {
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
padding: 15px;
|
|
background-color: #f0f8f0;
|
|
}
|
|
.status-info h4 {
|
|
margin-top: 0;
|
|
color: #2c5e2c;
|
|
}
|
|
.status-info p {
|
|
margin: 5px 0;
|
|
}
|
|
.button-group {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin: 20px 0;
|
|
}
|
|
.form-section {
|
|
background: #f9f9f9;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
border-radius: 8px;
|
|
border-left: 4px solid #4CAF50;
|
|
}
|
|
.form-group {
|
|
margin-bottom: 15px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.form-group label {
|
|
display: inline-block;
|
|
margin-bottom: 5px;
|
|
font-weight: bold;
|
|
min-width: 200px;
|
|
}
|
|
.form-group input[type="text"],
|
|
.form-group input[type="number"],
|
|
.form-group input[type="time"],
|
|
.form-group select {
|
|
padding: 8px 12px;
|
|
border-radius: 4px;
|
|
border: 1px solid #ccc;
|
|
font-size: 14px;
|
|
min-width: 150px;
|
|
}
|
|
.form-group input[type="checkbox"] {
|
|
width: 20px;
|
|
height: 20px;
|
|
margin: 0;
|
|
}
|
|
.form-group textarea {
|
|
width: 100%;
|
|
min-height: 80px;
|
|
padding: 8px 12px;
|
|
border-radius: 4px;
|
|
border: 1px solid #ccc;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
}
|
|
.description {
|
|
font-size: 12px;
|
|
color: #666;
|
|
margin-top: 5px;
|
|
font-style: italic;
|
|
}
|
|
.toggle-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.toggle-switch {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 44px;
|
|
height: 24px;
|
|
}
|
|
.toggle-switch input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
.slider {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: #ccc;
|
|
transition: .3s;
|
|
border-radius: 24px;
|
|
}
|
|
.slider:before {
|
|
position: absolute;
|
|
content: "";
|
|
height: 18px;
|
|
width: 18px;
|
|
left: 3px;
|
|
bottom: 3px;
|
|
background-color: white;
|
|
transition: .3s;
|
|
border-radius: 50%;
|
|
}
|
|
input:checked + .slider {
|
|
background-color: #4CAF50;
|
|
}
|
|
input:checked + .slider:before {
|
|
transform: translateX(20px);
|
|
}
|
|
button {
|
|
background-color: #4CAF50;
|
|
color: white;
|
|
padding: 12px 25px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
margin-top: 10px;
|
|
font-size: 16px;
|
|
transition: background-color 0.3s;
|
|
}
|
|
button:hover {
|
|
background-color: #45a049;
|
|
}
|
|
.flash-messages {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin-bottom: 15px;
|
|
}
|
|
.flash-messages li {
|
|
padding: 10px;
|
|
margin-bottom: 10px;
|
|
border-radius: 4px;
|
|
}
|
|
.flash-messages .success {
|
|
background-color: #d4edda;
|
|
color: #155724;
|
|
}
|
|
.flash-messages .error {
|
|
background-color: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
.filepath {
|
|
font-family: monospace;
|
|
background-color: #eee;
|
|
padding: 2px 5px;
|
|
border-radius: 3px;
|
|
font-size: 0.9em;
|
|
}
|
|
.action-buttons {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.action-button {
|
|
padding: 15px;
|
|
font-size: 14px;
|
|
}
|
|
.config-section {
|
|
margin-bottom: 30px;
|
|
}
|
|
.config-section h3 {
|
|
color: #4CAF50;
|
|
border-bottom: 2px solid #4CAF50;
|
|
padding-bottom: 5px;
|
|
margin-bottom: 15px;
|
|
}
|
|
.array-input {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
align-items: center;
|
|
}
|
|
.array-input input {
|
|
flex: 1;
|
|
min-width: 120px;
|
|
}
|
|
.add-item-btn {
|
|
background-color: #2196F3;
|
|
padding: 8px 15px;
|
|
font-size: 12px;
|
|
}
|
|
.remove-item-btn {
|
|
background-color: #f44336;
|
|
padding: 8px 15px;
|
|
font-size: 12px;
|
|
}
|
|
.array-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 5px;
|
|
}
|
|
.json-editor {
|
|
display: none;
|
|
}
|
|
.toggle-json-btn {
|
|
background-color: #ff9800;
|
|
margin-bottom: 10px;
|
|
}
|
|
.toggle-json-btn:hover {
|
|
background-color: #e68900;
|
|
}
|
|
.form-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
.form-col {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.json-container {
|
|
position: relative;
|
|
}
|
|
|
|
.json-validation {
|
|
margin-top: 10px;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
display: none;
|
|
}
|
|
|
|
.json-validation.success {
|
|
background-color: #d4edda;
|
|
color: #155724;
|
|
border: 1px solid #c3e6cb;
|
|
}
|
|
|
|
.json-validation.error {
|
|
background-color: #f8d7da;
|
|
color: #721c24;
|
|
border: 1px solid #f5c6cb;
|
|
}
|
|
|
|
.json-validation.warning {
|
|
background-color: #fff3cd;
|
|
color: #856404;
|
|
border: 1px solid #ffeaa7;
|
|
}
|
|
|
|
.json-actions {
|
|
margin-top: 10px;
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
.json-error-line {
|
|
background-color: #ffebee;
|
|
border-left: 3px solid #f44336;
|
|
padding: 2px 5px;
|
|
margin: 2px 0;
|
|
}
|
|
|
|
.json-error-message {
|
|
font-weight: bold;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.json-error-details {
|
|
font-size: 11px;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.json-container textarea {
|
|
border: 2px solid #ddd;
|
|
transition: border-color 0.3s;
|
|
}
|
|
|
|
.json-container textarea:focus {
|
|
border-color: #4CAF50;
|
|
outline: none;
|
|
}
|
|
|
|
.json-container textarea.error {
|
|
border-color: #f44336;
|
|
}
|
|
|
|
.json-container textarea.valid {
|
|
border-color: #4CAF50;
|
|
}
|
|
|
|
.save-button {
|
|
background-color: #4CAF50;
|
|
color: white;
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
transition: background-color 0.3s;
|
|
}
|
|
|
|
.save-button:hover {
|
|
background-color: #45a049;
|
|
}
|
|
|
|
.save-button:disabled {
|
|
background-color: #cccccc;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.json-actions {
|
|
margin-top: 15px;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
align-items: center;
|
|
}
|
|
|
|
.json-actions button {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.json-container {
|
|
position: relative;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.json-status {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
padding: 4px 8px;
|
|
border-radius: 3px;
|
|
font-size: 11px;
|
|
font-weight: bold;
|
|
color: white;
|
|
}
|
|
|
|
.json-status.valid {
|
|
background-color: #4CAF50;
|
|
}
|
|
|
|
.json-status.error {
|
|
background-color: #f44336;
|
|
}
|
|
|
|
.json-status.warning {
|
|
background-color: #ff9800;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>LED Matrix Configuration</h1>
|
|
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
<ul class="flash-messages">
|
|
{% for category, message in messages %}
|
|
<li class="{{ category }}">{{ message }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endif %}
|
|
{% endwith %}
|
|
|
|
<div class="tabs">
|
|
<button class="tab-link active" onclick="openTab(event, 'schedule')">Schedule</button>
|
|
<button class="tab-link" onclick="openTab(event, 'display')">Display Settings</button>
|
|
<button class="tab-link" onclick="openTab(event, 'sports')">Sports</button>
|
|
<button class="tab-link" onclick="openTab(event, 'weather')">Weather</button>
|
|
<button class="tab-link" onclick="openTab(event, 'stocks')">Stocks & Crypto</button>
|
|
<button class="tab-link" onclick="openTab(event, 'features')">Additional Features</button>
|
|
<button class="tab-link" onclick="openTab(event, 'music')">Music</button>
|
|
<button class="tab-link" onclick="openTab(event, 'calendar')">Calendar</button>
|
|
<button class="tab-link" onclick="openTab(event, 'news')">News Manager</button>
|
|
<button class="tab-link" onclick="openTab(event, 'secrets')">API Keys</button>
|
|
<button class="tab-link" onclick="openTab(event, 'actions')">Actions</button>
|
|
<button class="tab-link" onclick="openTab(event, 'raw-json')">Raw JSON</button>
|
|
<button class="tab-link" onclick="openTab(event, 'logs')">Logs</button>
|
|
</div>
|
|
|
|
<!-- Schedule Tab -->
|
|
<div id="schedule" class="tab-content" style="display: block;">
|
|
<div class="form-section">
|
|
<h2>Display Schedule</h2>
|
|
<p>Set the time for the display to be active. A restart is needed for changes to take effect.</p>
|
|
<form id="schedule-form" action="{{ url_for('save_schedule_route') }}" method="POST">
|
|
<div class="form-group">
|
|
<label for="schedule_enabled">Enable Schedule:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="schedule_enabled" name="schedule_enabled" {% if schedule_config.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
<span>Turn display on/off automatically</span>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="start_time">Display On Time:</label>
|
|
<input type="time" id="start_time" name="start_time" value="{{ schedule_config.start_time }}">
|
|
<div class="description">Time when the display should turn on</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="end_time">Display Off Time:</label>
|
|
<input type="time" id="end_time" name="end_time" value="{{ schedule_config.end_time }}">
|
|
<div class="description">Time when the display should turn off</div>
|
|
</div>
|
|
<button type="submit">Save Schedule</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Display Settings Tab -->
|
|
<div id="display" class="tab-content">
|
|
<div class="form-section">
|
|
<h2>Display Hardware Settings</h2>
|
|
<form action="{{ url_for('save_config_route') }}" method="POST" id="display-form">
|
|
<input type="hidden" name="config_type" value="main">
|
|
<input type="hidden" name="config_data" id="display-config-data">
|
|
|
|
<div class="form-row">
|
|
<div class="form-col">
|
|
<div class="form-group">
|
|
<label for="rows">Rows:</label>
|
|
<input type="number" id="rows" name="rows" value="{{ main_config.display.hardware.rows }}" min="1" max="64">
|
|
<div class="description">Number of LED rows</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="cols">Columns:</label>
|
|
<input type="number" id="cols" name="cols" value="{{ main_config.display.hardware.cols }}" min="1" max="128">
|
|
<div class="description">Number of LED columns</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="chain_length">Chain Length:</label>
|
|
<input type="number" id="chain_length" name="chain_length" value="{{ main_config.display.hardware.chain_length }}" min="1" max="8">
|
|
<div class="description">Number of LED panels chained together</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="parallel">Parallel:</label>
|
|
<input type="number" id="parallel" name="parallel" value="{{ main_config.display.hardware.parallel }}" min="1" max="4">
|
|
<div class="description">Number of parallel chains</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-col">
|
|
<div class="form-group">
|
|
<label for="brightness">Brightness:</label>
|
|
<input type="number" id="brightness" name="brightness" value="{{ main_config.display.hardware.brightness }}" min="1" max="100">
|
|
<div class="description">LED brightness (1-100)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="hardware_mapping">Hardware Mapping:</label>
|
|
<select id="hardware_mapping" name="hardware_mapping">
|
|
<option value="adafruit-hat-pwm" {% if main_config.display.hardware.hardware_mapping == "adafruit-hat-pwm" %}selected{% endif %}>Adafruit HAT PWM</option>
|
|
<option value="adafruit-hat" {% if main_config.display.hardware.hardware_mapping == "adafruit-hat" %}selected{% endif %}>Adafruit HAT</option>
|
|
<option value="regular" {% if main_config.display.hardware.hardware_mapping == "regular" %}selected{% endif %}>Regular</option>
|
|
<option value="regular-pi1" {% if main_config.display.hardware.hardware_mapping == "regular-pi1" %}selected{% endif %}>Regular Pi1</option>
|
|
</select>
|
|
<div class="description">Hardware mapping type</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="gpio_slowdown">GPIO Slowdown:</label>
|
|
<input type="number" id="gpio_slowdown" name="gpio_slowdown" value="{{ main_config.display.runtime.gpio_slowdown }}" min="0" max="5">
|
|
<div class="description">GPIO slowdown factor (0-5)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="scan_mode">Scan Mode:</label>
|
|
<input type="number" id="scan_mode" name="scan_mode" value="{{ main_config.display.hardware.scan_mode }}" min="0" max="1">
|
|
<div class="description">Scan mode for LED matrix (0-1)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="pwm_bits">PWM Bits:</label>
|
|
<input type="number" id="pwm_bits" name="pwm_bits" value="{{ main_config.display.hardware.pwm_bits }}" min="1" max="11">
|
|
<div class="description">PWM bits for brightness control (1-11)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="pwm_dither_bits">PWM Dither Bits:</label>
|
|
<input type="number" id="pwm_dither_bits" name="pwm_dither_bits" value="{{ main_config.display.hardware.pwm_dither_bits }}" min="0" max="4">
|
|
<div class="description">PWM dither bits (0-4)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="pwm_lsb_nanoseconds">PWM LSB Nanoseconds:</label>
|
|
<input type="number" id="pwm_lsb_nanoseconds" name="pwm_lsb_nanoseconds" value="{{ main_config.display.hardware.pwm_lsb_nanoseconds }}" min="50" max="500">
|
|
<div class="description">PWM LSB nanoseconds (50-500)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="disable_hardware_pulsing">Disable Hardware Pulsing:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="disable_hardware_pulsing" name="disable_hardware_pulsing" {% if main_config.display.hardware.disable_hardware_pulsing %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Disable hardware pulsing</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="inverse_colors">Inverse Colors:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="inverse_colors" name="inverse_colors" {% if main_config.display.hardware.inverse_colors %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Inverse color display</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="show_refresh_rate">Show Refresh Rate:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="show_refresh_rate" name="show_refresh_rate" {% if main_config.display.hardware.show_refresh_rate %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Show refresh rate on display</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="limit_refresh_rate_hz">Limit Refresh Rate (Hz):</label>
|
|
<input type="number" id="limit_refresh_rate_hz" name="limit_refresh_rate_hz" value="{{ main_config.display.hardware.limit_refresh_rate_hz }}" min="1" max="1000">
|
|
<div class="description">Limit refresh rate in Hz (1-1000)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="use_short_date_format">Use Short Date Format:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="use_short_date_format" name="use_short_date_format" {% if main_config.display.use_short_date_format %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Use short date format for display</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit">Save Display Settings</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h2>Display Durations</h2>
|
|
<p>Set how long each content type displays on the LED matrix.</p>
|
|
<form action="{{ url_for('save_config_route') }}" method="POST" id="durations-form">
|
|
<input type="hidden" name="config_type" value="main">
|
|
<input type="hidden" name="config_data" id="durations-config-data">
|
|
|
|
<div class="form-row">
|
|
<div class="form-col">
|
|
<div class="form-group">
|
|
<label for="clock_duration">Clock Duration (seconds):</label>
|
|
<input type="number" id="clock_duration" name="clock_duration" value="{{ main_config.display.display_durations.clock }}" min="5" max="120">
|
|
<div class="description">How long to show clock</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="weather_duration">Weather Duration (seconds):</label>
|
|
<input type="number" id="weather_duration" name="weather_duration" value="{{ main_config.display.display_durations.weather }}" min="5" max="120">
|
|
<div class="description">How long to show weather</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="stocks_duration">Stocks Duration (seconds):</label>
|
|
<input type="number" id="stocks_duration" name="stocks_duration" value="{{ main_config.display.display_durations.stocks }}" min="5" max="120">
|
|
<div class="description">How long to show stocks</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="music_duration">Music Duration (seconds):</label>
|
|
<input type="number" id="music_duration" name="music_duration" value="{{ main_config.display.display_durations.music }}" min="5" max="120">
|
|
<div class="description">How long to show music info</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-col">
|
|
<div class="form-group">
|
|
<label for="calendar_duration">Calendar Duration (seconds):</label>
|
|
<input type="number" id="calendar_duration" name="calendar_duration" value="{{ main_config.display.display_durations.calendar }}" min="5" max="120">
|
|
<div class="description">How long to show calendar events</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="youtube_duration">YouTube Duration (seconds):</label>
|
|
<input type="number" id="youtube_duration" name="youtube_duration" value="{{ main_config.display.display_durations.youtube }}" min="5" max="120">
|
|
<div class="description">How long to show YouTube info</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="text_display_duration">Text Display Duration (seconds):</label>
|
|
<input type="number" id="text_display_duration" name="text_display_duration" value="{{ main_config.display.display_durations.text_display }}" min="5" max="120">
|
|
<div class="description">How long to show custom text</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="of_the_day_duration">Of The Day Duration (seconds):</label>
|
|
<input type="number" id="of_the_day_duration" name="of_the_day_duration" value="{{ main_config.display.display_durations.of_the_day }}" min="5" max="120">
|
|
<div class="description">How long to show word of the day</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="hourly_forecast_duration">Hourly Forecast Duration (seconds):</label>
|
|
<input type="number" id="hourly_forecast_duration" name="hourly_forecast_duration" value="{{ main_config.display.display_durations.hourly_forecast }}" min="5" max="120">
|
|
<div class="description">How long to show hourly forecast</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="daily_forecast_duration">Daily Forecast Duration (seconds):</label>
|
|
<input type="number" id="daily_forecast_duration" name="daily_forecast_duration" value="{{ main_config.display.display_durations.daily_forecast }}" min="5" max="120">
|
|
<div class="description">How long to show daily forecast</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="stock_news_duration">Stock News Duration (seconds):</label>
|
|
<input type="number" id="stock_news_duration" name="stock_news_duration" value="{{ main_config.display.display_durations.stock_news }}" min="5" max="120">
|
|
<div class="description">How long to show stock news</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="odds_ticker_duration">Odds Ticker Duration (seconds):</label>
|
|
<input type="number" id="odds_ticker_duration" name="odds_ticker_duration" value="{{ main_config.display.display_durations.odds_ticker }}" min="5" max="120">
|
|
<div class="description">How long to show odds ticker</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-col">
|
|
<h4>Sports Durations</h4>
|
|
<div class="form-group">
|
|
<label for="nhl_live_duration">NHL Live Duration (seconds):</label>
|
|
<input type="number" id="nhl_live_duration" name="nhl_live_duration" value="{{ main_config.display.display_durations.nhl_live }}" min="5" max="120">
|
|
<div class="description">How long to show NHL live games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nhl_recent_duration">NHL Recent Duration (seconds):</label>
|
|
<input type="number" id="nhl_recent_duration" name="nhl_recent_duration" value="{{ main_config.display.display_durations.nhl_recent }}" min="5" max="120">
|
|
<div class="description">How long to show NHL recent games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nhl_upcoming_duration">NHL Upcoming Duration (seconds):</label>
|
|
<input type="number" id="nhl_upcoming_duration" name="nhl_upcoming_duration" value="{{ main_config.display.display_durations.nhl_upcoming }}" min="5" max="120">
|
|
<div class="description">How long to show NHL upcoming games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nba_live_duration">NBA Live Duration (seconds):</label>
|
|
<input type="number" id="nba_live_duration" name="nba_live_duration" value="{{ main_config.display.display_durations.nba_live }}" min="5" max="120">
|
|
<div class="description">How long to show NBA live games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nba_recent_duration">NBA Recent Duration (seconds):</label>
|
|
<input type="number" id="nba_recent_duration" name="nba_recent_duration" value="{{ main_config.display.display_durations.nba_recent }}" min="5" max="120">
|
|
<div class="description">How long to show NBA recent games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nba_upcoming_duration">NBA Upcoming Duration (seconds):</label>
|
|
<input type="number" id="nba_upcoming_duration" name="nba_upcoming_duration" value="{{ main_config.display.display_durations.nba_upcoming }}" min="5" max="120">
|
|
<div class="description">How long to show NBA upcoming games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nfl_live_duration">NFL Live Duration (seconds):</label>
|
|
<input type="number" id="nfl_live_duration" name="nfl_live_duration" value="{{ main_config.display.display_durations.nfl_live }}" min="5" max="120">
|
|
<div class="description">How long to show NFL live games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nfl_recent_duration">NFL Recent Duration (seconds):</label>
|
|
<input type="number" id="nfl_recent_duration" name="nfl_recent_duration" value="{{ main_config.display.display_durations.nfl_recent }}" min="5" max="120">
|
|
<div class="description">How long to show NFL recent games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nfl_upcoming_duration">NFL Upcoming Duration (seconds):</label>
|
|
<input type="number" id="nfl_upcoming_duration" name="nfl_upcoming_duration" value="{{ main_config.display.display_durations.nfl_upcoming }}" min="5" max="120">
|
|
<div class="description">How long to show NFL upcoming games</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-col">
|
|
<h4>More Sports Durations</h4>
|
|
<div class="form-group">
|
|
<label for="ncaa_fb_live_duration">NCAA FB Live Duration (seconds):</label>
|
|
<input type="number" id="ncaa_fb_live_duration" name="ncaa_fb_live_duration" value="{{ main_config.display.display_durations.ncaa_fb_live }}" min="5" max="120">
|
|
<div class="description">How long to show NCAA FB live games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_fb_recent_duration">NCAA FB Recent Duration (seconds):</label>
|
|
<input type="number" id="ncaa_fb_recent_duration" name="ncaa_fb_recent_duration" value="{{ main_config.display.display_durations.ncaa_fb_recent }}" min="5" max="120">
|
|
<div class="description">How long to show NCAA FB recent games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_fb_upcoming_duration">NCAA FB Upcoming Duration (seconds):</label>
|
|
<input type="number" id="ncaa_fb_upcoming_duration" name="ncaa_fb_upcoming_duration" value="{{ main_config.display.display_durations.ncaa_fb_upcoming }}" min="5" max="120">
|
|
<div class="description">How long to show NCAA FB upcoming games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_baseball_live_duration">NCAA Baseball Live Duration (seconds):</label>
|
|
<input type="number" id="ncaa_baseball_live_duration" name="ncaa_baseball_live_duration" value="{{ main_config.display.display_durations.ncaa_baseball_live }}" min="5" max="120">
|
|
<div class="description">How long to show NCAA Baseball live games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_baseball_recent_duration">NCAA Baseball Recent Duration (seconds):</label>
|
|
<input type="number" id="ncaa_baseball_recent_duration" name="ncaa_baseball_recent_duration" value="{{ main_config.display.display_durations.ncaa_baseball_recent }}" min="5" max="120">
|
|
<div class="description">How long to show NCAA Baseball recent games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_baseball_upcoming_duration">NCAA Baseball Upcoming Duration (seconds):</label>
|
|
<input type="number" id="ncaa_baseball_upcoming_duration" name="ncaa_baseball_upcoming_duration" value="{{ main_config.display.display_durations.ncaa_baseball_upcoming }}" min="5" max="120">
|
|
<div class="description">How long to show NCAA Baseball upcoming games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="mlb_live_duration">MLB Live Duration (seconds):</label>
|
|
<input type="number" id="mlb_live_duration" name="mlb_live_duration" value="{{ main_config.display.display_durations.mlb_live }}" min="5" max="120">
|
|
<div class="description">How long to show MLB live games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="mlb_recent_duration">MLB Recent Duration (seconds):</label>
|
|
<input type="number" id="mlb_recent_duration" name="mlb_recent_duration" value="{{ main_config.display.display_durations.mlb_recent }}" min="5" max="120">
|
|
<div class="description">How long to show MLB recent games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="mlb_upcoming_duration">MLB Upcoming Duration (seconds):</label>
|
|
<input type="number" id="mlb_upcoming_duration" name="mlb_upcoming_duration" value="{{ main_config.display.display_durations.mlb_upcoming }}" min="5" max="120">
|
|
<div class="description">How long to show MLB upcoming games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="milb_live_duration">MiLB Live Duration (seconds):</label>
|
|
<input type="number" id="milb_live_duration" name="milb_live_duration" value="{{ main_config.display.display_durations.milb_live }}" min="5" max="120">
|
|
<div class="description">How long to show MiLB live games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="milb_recent_duration">MiLB Recent Duration (seconds):</label>
|
|
<input type="number" id="milb_recent_duration" name="milb_recent_duration" value="{{ main_config.display.display_durations.milb_recent }}" min="5" max="120">
|
|
<div class="description">How long to show MiLB recent games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="milb_upcoming_duration">MiLB Upcoming Duration (seconds):</label>
|
|
<input type="number" id="milb_upcoming_duration" name="milb_upcoming_duration" value="{{ main_config.display.display_durations.milb_upcoming }}" min="5" max="120">
|
|
<div class="description">How long to show MiLB upcoming games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="soccer_live_duration">Soccer Live Duration (seconds):</label>
|
|
<input type="number" id="soccer_live_duration" name="soccer_live_duration" value="{{ main_config.display.display_durations.soccer_live }}" min="5" max="120">
|
|
<div class="description">How long to show Soccer live games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="soccer_recent_duration">Soccer Recent Duration (seconds):</label>
|
|
<input type="number" id="soccer_recent_duration" name="soccer_recent_duration" value="{{ main_config.display.display_durations.soccer_recent }}" min="5" max="120">
|
|
<div class="description">How long to show Soccer recent games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="soccer_upcoming_duration">Soccer Upcoming Duration (seconds):</label>
|
|
<input type="number" id="soccer_upcoming_duration" name="soccer_upcoming_duration" value="{{ main_config.display.display_durations.soccer_upcoming }}" min="5" max="120">
|
|
<div class="description">How long to show Soccer upcoming games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaam_basketball_live_duration">NCAA Basketball Live Duration (seconds):</label>
|
|
<input type="number" id="ncaam_basketball_live_duration" name="ncaam_basketball_live_duration" value="{{ main_config.display.display_durations.ncaam_basketball_live }}" min="5" max="120">
|
|
<div class="description">How long to show NCAA Basketball live games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaam_basketball_recent_duration">NCAA Basketball Recent Duration (seconds):</label>
|
|
<input type="number" id="ncaam_basketball_recent_duration" name="ncaam_basketball_recent_duration" value="{{ main_config.display.display_durations.ncaam_basketball_recent }}" min="5" max="120">
|
|
<div class="description">How long to show NCAA Basketball recent games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaam_basketball_upcoming_duration">NCAA Basketball Upcoming Duration (seconds):</label>
|
|
<input type="number" id="ncaam_basketball_upcoming_duration" name="ncaam_basketball_upcoming_duration" value="{{ main_config.display.display_durations.ncaam_basketball_upcoming }}" min="5" max="120">
|
|
<div class="description">How long to show NCAA Basketball upcoming games</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit">Save Display Durations</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h2>General Settings</h2>
|
|
<form action="{{ url_for('save_config_route') }}" method="POST" id="general-form">
|
|
<input type="hidden" name="config_type" value="main">
|
|
<input type="hidden" name="config_data" id="general-config-data">
|
|
|
|
<div class="form-group">
|
|
<label for="web_display_autostart">Auto-start Display:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="web_display_autostart" name="web_display_autostart" {% if main_config.web_display_autostart %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
<span>Automatically start display on boot</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="timezone">Timezone:</label>
|
|
<select id="timezone" name="timezone">
|
|
<option value="America/Chicago" {% if main_config.timezone == "America/Chicago" %}selected{% endif %}>Central Time</option>
|
|
<option value="America/New_York" {% if main_config.timezone == "America/New_York" %}selected{% endif %}>Eastern Time</option>
|
|
<option value="America/Denver" {% if main_config.timezone == "America/Denver" %}selected{% endif %}>Mountain Time</option>
|
|
<option value="America/Los_Angeles" {% if main_config.timezone == "America/Los_Angeles" %}selected{% endif %}>Pacific Time</option>
|
|
<option value="UTC" {% if main_config.timezone == "UTC" %}selected{% endif %}>UTC</option>
|
|
</select>
|
|
<div class="description">System timezone</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="location_country">Country:</label>
|
|
<input type="text" id="location_country" name="location_country" value="{{ main_config.location.country }}" placeholder="US">
|
|
<div class="description">Country code for location</div>
|
|
</div>
|
|
|
|
<button type="submit">Save General Settings</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sports Tab -->
|
|
<div id="sports" class="tab-content">
|
|
<div class="form-section">
|
|
<h2>Sports Configuration</h2>
|
|
<p>Configure which sports leagues to display and their settings.</p>
|
|
|
|
<div class="config-section">
|
|
<h3>MLB (Baseball)</h3>
|
|
<div class="form-group">
|
|
<label for="mlb_enabled">Enable MLB:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="mlb_enabled" name="mlb_enabled" {% if main_config.mlb.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="mlb_favorites">Favorite Teams:</label>
|
|
<div class="array-input">
|
|
<input type="text" id="mlb_favorites" name="mlb_favorites" value="{{ main_config.mlb.favorite_teams|join(', ') }}" placeholder="TB, TEX">
|
|
<button type="button" class="add-item-btn" onclick="addArrayItem('mlb_favorites')">Add</button>
|
|
</div>
|
|
<div class="description">Comma-separated team abbreviations (e.g., TB, TEX)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="mlb_live_priority">Live Priority:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="mlb_live_priority" name="mlb_live_priority" {% if main_config.mlb.live_priority %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Prioritize live/in-progress games over finished/upcoming games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="mlb_live_game_duration">Live Game Duration (minutes):</label>
|
|
<input type="number" id="mlb_live_game_duration" name="mlb_live_game_duration" value="{{ main_config.mlb.live_game_duration }}" min="5" max="300">
|
|
<div class="description">How long to display each live game</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="mlb_show_odds">Show Odds:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="mlb_show_odds" name="mlb_show_odds" {% if main_config.mlb.show_odds %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Display betting odds for games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="mlb_show_favorite_teams_only">Show Favorite Teams Only:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="mlb_show_favorite_teams_only" name="mlb_show_favorite_teams_only" {% if main_config.mlb.show_favorite_teams_only %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Only display games involving your favorite teams</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="mlb_test_mode">Test Mode:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="mlb_test_mode" name="mlb_test_mode" {% if main_config.mlb.test_mode %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Enable test mode for MLB</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="mlb_update_interval_seconds">Update Interval (seconds):</label>
|
|
<input type="number" id="mlb_update_interval_seconds" name="mlb_update_interval_seconds" value="{{ main_config.mlb.update_interval_seconds }}" min="60" max="7200">
|
|
<div class="description">How often to update MLB data</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="mlb_live_update_interval">Live Update Interval (seconds):</label>
|
|
<input type="number" id="mlb_live_update_interval" name="mlb_live_update_interval" value="{{ main_config.mlb.live_update_interval }}" min="10" max="300">
|
|
<div class="description">How often to update live MLB games</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="mlb_live_odds_update_interval">Live Odds Update Interval (seconds):</label>
|
|
<input type="number" id="mlb_live_odds_update_interval" name="mlb_live_odds_update_interval" value="{{ main_config.mlb.live_odds_update_interval }}" min="60" max="3600">
|
|
<div class="description">How often to update live odds for MLB</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="mlb_odds_update_interval">Odds Update Interval (seconds):</label>
|
|
<input type="number" id="mlb_odds_update_interval" name="mlb_odds_update_interval" value="{{ main_config.mlb.odds_update_interval }}" min="60" max="3600">
|
|
<div class="description">How often to update odds for MLB</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="mlb_recent_update_interval">Recent Update Interval (seconds):</label>
|
|
<input type="number" id="mlb_recent_update_interval" name="mlb_recent_update_interval" value="{{ main_config.mlb.recent_update_interval }}" min="60" max="7200">
|
|
<div class="description">How often to update recent MLB games</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="mlb_upcoming_update_interval">Upcoming Update Interval (seconds):</label>
|
|
<input type="number" id="mlb_upcoming_update_interval" name="mlb_upcoming_update_interval" value="{{ main_config.mlb.upcoming_update_interval }}" min="60" max="7200">
|
|
<div class="description">How often to update upcoming MLB games</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="mlb_recent_games_to_show">Recent Games to Show:</label>
|
|
<input type="number" id="mlb_recent_games_to_show" name="mlb_recent_games_to_show" value="{{ main_config.mlb.recent_games_to_show }}" min="0" max="10">
|
|
<div class="description">Number of recent games to display</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="mlb_upcoming_games_to_show">Upcoming Games to Show:</label>
|
|
<input type="number" id="mlb_upcoming_games_to_show" name="mlb_upcoming_games_to_show" value="{{ main_config.mlb.upcoming_games_to_show }}" min="0" max="10">
|
|
<div class="description">Number of upcoming games to display</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="mlb_show_records">Show Records:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="mlb_show_records" name="mlb_show_records" {% if main_config.mlb.show_records %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Show team records</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>NFL (Football)</h3>
|
|
<div class="form-group">
|
|
<label for="nfl_enabled">Enable NFL:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="nfl_enabled" name="nfl_enabled" {% if main_config.nfl_scoreboard.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nfl_favorites">Favorite Teams:</label>
|
|
<div class="array-input">
|
|
<input type="text" id="nfl_favorites" name="nfl_favorites" value="{{ main_config.nfl_scoreboard.favorite_teams|join(', ') }}" placeholder="TB, DAL">
|
|
<button type="button" class="add-item-btn" onclick="addArrayItem('nfl_favorites')">Add</button>
|
|
</div>
|
|
<div class="description">Comma-separated team abbreviations</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nfl_live_priority">Live Priority:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="nfl_live_priority" name="nfl_live_priority" {% if main_config.nfl_scoreboard.live_priority %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Prioritize live/in-progress games over finished/upcoming games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nfl_live_game_duration">Live Game Duration (minutes):</label>
|
|
<input type="number" id="nfl_live_game_duration" name="nfl_live_game_duration" value="{{ main_config.nfl_scoreboard.live_game_duration }}" min="5" max="300">
|
|
<div class="description">How long to display each live game</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nfl_show_odds">Show Odds:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="nfl_show_odds" name="nfl_show_odds" {% if main_config.nfl_scoreboard.show_odds %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Display betting odds for games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nfl_show_favorite_teams_only">Show Favorite Teams Only:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="nfl_show_favorite_teams_only" name="nfl_show_favorite_teams_only" {% if main_config.nfl_scoreboard.show_favorite_teams_only %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Only display games involving your favorite teams</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>NBA (Basketball)</h3>
|
|
<div class="form-group">
|
|
<label for="nba_enabled">Enable NBA:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="nba_enabled" name="nba_enabled" {% if main_config.nba_scoreboard.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nba_favorites">Favorite Teams:</label>
|
|
<div class="array-input">
|
|
<input type="text" id="nba_favorites" name="nba_favorites" value="{{ main_config.nba_scoreboard.favorite_teams|join(', ') }}" placeholder="DAL">
|
|
<button type="button" class="add-item-btn" onclick="addArrayItem('nba_favorites')">Add</button>
|
|
</div>
|
|
<div class="description">Comma-separated team abbreviations</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nba_live_priority">Live Priority:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="nba_live_priority" name="nba_live_priority" {% if main_config.nba_scoreboard.live_priority %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Prioritize live/in-progress games over finished/upcoming games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nba_live_game_duration">Live Game Duration (minutes):</label>
|
|
<input type="number" id="nba_live_game_duration" name="nba_live_game_duration" value="{{ main_config.nba_scoreboard.live_game_duration }}" min="5" max="300">
|
|
<div class="description">How long to display each live game</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nba_show_odds">Show Odds:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="nba_show_odds" name="nba_show_odds" {% if main_config.nba_scoreboard.show_odds %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Display betting odds for games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nba_recent_games_to_show">Recent Games to Show:</label>
|
|
<input type="number" id="nba_recent_games_to_show" name="nba_recent_games_to_show" value="{{ main_config.nba_scoreboard.recent_games_to_show }}" min="1" max="10">
|
|
<div class="description">Number of most recent games to display (default: 5)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nba_upcoming_games_to_show">Upcoming Games to Show:</label>
|
|
<input type="number" id="nba_upcoming_games_to_show" name="nba_upcoming_games_to_show" value="{{ main_config.nba_scoreboard.upcoming_games_to_show }}" min="1" max="10">
|
|
<div class="description">Number of upcoming games to display (default: 5)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nba_show_favorite_teams_only">Show Favorite Teams Only:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="nba_show_favorite_teams_only" name="nba_show_favorite_teams_only" {% if main_config.nba_scoreboard.show_favorite_teams_only %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Only display games involving your favorite teams</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>NHL (Hockey)</h3>
|
|
<div class="form-group">
|
|
<label for="nhl_enabled">Enable NHL:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="nhl_enabled" name="nhl_enabled" {% if main_config.nhl_scoreboard.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nhl_favorites">Favorite Teams:</label>
|
|
<div class="array-input">
|
|
<input type="text" id="nhl_favorites" name="nhl_favorites" value="{{ main_config.nhl_scoreboard.favorite_teams|join(', ') }}" placeholder="TB">
|
|
<button type="button" class="add-item-btn" onclick="addArrayItem('nhl_favorites')">Add</button>
|
|
</div>
|
|
<div class="description">Comma-separated team abbreviations</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nhl_live_priority">Live Priority:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="nhl_live_priority" name="nhl_live_priority" {% if main_config.nhl_scoreboard.live_priority %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Prioritize live/in-progress games over finished/upcoming games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nhl_live_game_duration">Live Game Duration (minutes):</label>
|
|
<input type="number" id="nhl_live_game_duration" name="nhl_live_game_duration" value="{{ main_config.nhl_scoreboard.live_game_duration }}" min="5" max="300">
|
|
<div class="description">How long to display each live game</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nhl_show_odds">Show Odds:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="nhl_show_odds" name="nhl_show_odds" {% if main_config.nhl_scoreboard.show_odds %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Display betting odds for games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nhl_show_favorite_teams_only">Show Favorite Teams Only:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="nhl_show_favorite_teams_only" name="nhl_show_favorite_teams_only" {% if main_config.nhl_scoreboard.show_favorite_teams_only %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Only display games involving your favorite teams</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nhl_recent_games_to_show">Recent Games to Show:</label>
|
|
<input type="number" id="nhl_recent_games_to_show" name="nhl_recent_games_to_show" value="{{ main_config.nhl_scoreboard.recent_games_to_show }}" min="1" max="10">
|
|
<div class="description">Number of most recent games to display (default: 5)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nhl_upcoming_games_to_show">Upcoming Games to Show:</label>
|
|
<input type="number" id="nhl_upcoming_games_to_show" name="nhl_upcoming_games_to_show" value="{{ main_config.nhl_scoreboard.upcoming_games_to_show }}" min="1" max="10">
|
|
<div class="description">Number of upcoming games to display (default: 5)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>NCAA Football</h3>
|
|
<div class="form-group">
|
|
<label for="ncaa_fb_enabled">Enable NCAA Football:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="ncaa_fb_enabled" name="ncaa_fb_enabled" {% if main_config.ncaa_fb_scoreboard.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_fb_favorites">Favorite Teams:</label>
|
|
<div class="array-input">
|
|
<input type="text" id="ncaa_fb_favorites" name="ncaa_fb_favorites" value="{{ main_config.ncaa_fb_scoreboard.favorite_teams|join(', ') }}" placeholder="UGA, AUB">
|
|
<button type="button" class="add-item-btn" onclick="addArrayItem('ncaa_fb_favorites')">Add</button>
|
|
</div>
|
|
<div class="description">Comma-separated team abbreviations</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_fb_live_priority">Live Priority:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="ncaa_fb_live_priority" name="ncaa_fb_live_priority" {% if main_config.ncaa_fb_scoreboard.live_priority %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Prioritize live/in-progress games over finished/upcoming games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_fb_live_game_duration">Live Game Duration (minutes):</label>
|
|
<input type="number" id="ncaa_fb_live_game_duration" name="ncaa_fb_live_game_duration" value="{{ main_config.ncaa_fb_scoreboard.live_game_duration }}" min="5" max="300">
|
|
<div class="description">How long to display each live game</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_fb_show_odds">Show Odds:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="ncaa_fb_show_odds" name="ncaa_fb_show_odds" {% if main_config.ncaa_fb_scoreboard.show_odds %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Display betting odds for games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_fb_show_favorite_teams_only">Show Favorite Teams Only:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="ncaa_fb_show_favorite_teams_only" name="ncaa_fb_show_favorite_teams_only" {% if main_config.ncaa_fb_scoreboard.show_favorite_teams_only %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Only display games involving your favorite teams</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_fb_show_records">Show Team Records:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="ncaa_fb_show_records" name="ncaa_fb_show_records" {% if main_config.ncaa_fb_scoreboard.show_records %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Display team win-loss records in bottom corners</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_fb_show_ranking">Show Team Rankings:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="ncaa_fb_show_ranking" name="ncaa_fb_show_ranking" {% if main_config.ncaa_fb_scoreboard.show_ranking %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Display AP Top 25 rankings instead of records for ranked teams</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>NCAA Baseball</h3>
|
|
<div class="form-group">
|
|
<label for="ncaa_baseball_enabled">Enable NCAA Baseball:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="ncaa_baseball_enabled" name="ncaa_baseball_enabled" {% if main_config.ncaa_baseball_scoreboard.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_baseball_favorites">Favorite Teams:</label>
|
|
<div class="array-input">
|
|
<input type="text" id="ncaa_baseball_favorites" name="ncaa_baseball_favorites" value="{{ main_config.ncaa_baseball_scoreboard.favorite_teams|join(', ') }}" placeholder="UGA, AUB">
|
|
<button type="button" class="add-item-btn" onclick="addArrayItem('ncaa_baseball_favorites')">Add</button>
|
|
</div>
|
|
<div class="description">Comma-separated team abbreviations</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_baseball_live_priority">Live Priority:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="ncaa_baseball_live_priority" name="ncaa_baseball_live_priority" {% if main_config.ncaa_baseball_scoreboard.live_priority %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Prioritize live/in-progress games over finished/upcoming games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_baseball_live_game_duration">Live Game Duration (minutes):</label>
|
|
<input type="number" id="ncaa_baseball_live_game_duration" name="ncaa_baseball_live_game_duration" value="{{ main_config.ncaa_baseball_scoreboard.live_game_duration }}" min="5" max="300">
|
|
<div class="description">How long to display each live game</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_baseball_show_odds">Show Odds:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="ncaa_baseball_show_odds" name="ncaa_baseball_show_odds" {% if main_config.ncaa_baseball_scoreboard.show_odds %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Display betting odds for games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_baseball_recent_games_to_show">Recent Games to Show:</label>
|
|
<input type="number" id="ncaa_baseball_recent_games_to_show" name="ncaa_baseball_recent_games_to_show" value="{{ main_config.ncaa_baseball_scoreboard.recent_games_to_show }}" min="1" max="10">
|
|
<div class="description">Number of most recent games to display (default: 5)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_baseball_upcoming_games_to_show">Upcoming Games to Show:</label>
|
|
<input type="number" id="ncaa_baseball_upcoming_games_to_show" name="ncaa_baseball_upcoming_games_to_show" value="{{ main_config.ncaa_baseball_scoreboard.upcoming_games_to_show }}" min="1" max="10">
|
|
<div class="description">Number of upcoming games to display (default: 5)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaa_baseball_show_favorite_teams_only">Show Favorite Teams Only:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="ncaa_baseball_show_favorite_teams_only" name="ncaa_baseball_show_favorite_teams_only" {% if main_config.ncaa_baseball_scoreboard.show_favorite_teams_only %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Only display games involving your favorite teams</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>NCAA Basketball</h3>
|
|
<div class="form-group">
|
|
<label for="ncaam_basketball_enabled">Enable NCAA Basketball:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="ncaam_basketball_enabled" name="ncaam_basketball_enabled" {% if main_config.ncaam_basketball_scoreboard.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaam_basketball_favorites">Favorite Teams:</label>
|
|
<div class="array-input">
|
|
<input type="text" id="ncaam_basketball_favorites" name="ncaam_basketball_favorites" value="{{ main_config.ncaam_basketball_scoreboard.favorite_teams|join(', ') }}" placeholder="UGA, AUB">
|
|
<button type="button" class="add-item-btn" onclick="addArrayItem('ncaam_basketball_favorites')">Add</button>
|
|
</div>
|
|
<div class="description">Comma-separated team abbreviations</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaam_basketball_live_priority">Live Priority:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="ncaam_basketball_live_priority" name="ncaam_basketball_live_priority" {% if main_config.ncaam_basketball_scoreboard.live_priority %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Prioritize live/in-progress games over finished/upcoming games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaam_basketball_live_game_duration">Live Game Duration (minutes):</label>
|
|
<input type="number" id="ncaam_basketball_live_game_duration" name="ncaam_basketball_live_game_duration" value="{{ main_config.ncaam_basketball_scoreboard.live_game_duration }}" min="5" max="300">
|
|
<div class="description">How long to display each live game</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaam_basketball_show_odds">Show Odds:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="ncaam_basketball_show_odds" name="ncaam_basketball_show_odds" {% if main_config.ncaam_basketball_scoreboard.show_odds %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Display betting odds for games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaam_basketball_recent_games_to_show">Recent Games to Show:</label>
|
|
<input type="number" id="ncaam_basketball_recent_games_to_show" name="ncaam_basketball_recent_games_to_show" value="{{ main_config.ncaam_basketball_scoreboard.recent_games_to_show }}" min="1" max="10">
|
|
<div class="description">Number of most recent games to display (default: 5)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaam_basketball_upcoming_games_to_show">Upcoming Games to Show:</label>
|
|
<input type="number" id="ncaam_basketball_upcoming_games_to_show" name="ncaam_basketball_upcoming_games_to_show" value="{{ main_config.ncaam_basketball_scoreboard.upcoming_games_to_show }}" min="1" max="10">
|
|
<div class="description">Number of upcoming games to display (default: 5)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="ncaam_basketball_show_favorite_teams_only">Show Favorite Teams Only:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="ncaam_basketball_show_favorite_teams_only" name="ncaam_basketball_show_favorite_teams_only" {% if main_config.ncaam_basketball_scoreboard.show_favorite_teams_only %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Only display games involving your favorite teams</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>MiLB (Minor League Baseball)</h3>
|
|
<div class="form-group">
|
|
<label for="milb_enabled">Enable MiLB:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="milb_enabled" name="milb_enabled" {% if main_config.milb.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="milb_favorites">Favorite Teams:</label>
|
|
<div class="array-input">
|
|
<input type="text" id="milb_favorites" name="milb_favorites" value="{{ main_config.milb.favorite_teams|join(', ') }}" placeholder="TAM">
|
|
<button type="button" class="add-item-btn" onclick="addArrayItem('milb_favorites')">Add</button>
|
|
</div>
|
|
<div class="description">Comma-separated team abbreviations</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="milb_live_priority">Live Priority:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="milb_live_priority" name="milb_live_priority" {% if main_config.milb.live_priority %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Prioritize live/in-progress games over finished/upcoming games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="milb_live_game_duration">Live Game Duration (minutes):</label>
|
|
<input type="number" id="milb_live_game_duration" name="milb_live_game_duration" value="{{ main_config.milb.live_game_duration }}" min="5" max="300">
|
|
<div class="description">How long to display each live game</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="milb_test_mode">Test Mode:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="milb_test_mode" name="milb_test_mode" {% if main_config.milb.test_mode %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Enable test mode for MiLB</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="milb_update_interval_seconds">Update Interval (seconds):</label>
|
|
<input type="number" id="milb_update_interval_seconds" name="milb_update_interval_seconds" value="{{ main_config.milb.update_interval_seconds }}" min="60" max="7200">
|
|
<div class="description">How often to update MiLB data</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="milb_live_update_interval">Live Update Interval (seconds):</label>
|
|
<input type="number" id="milb_live_update_interval" name="milb_live_update_interval" value="{{ main_config.milb.live_update_interval }}" min="10" max="300">
|
|
<div class="description">How often to update live MiLB games</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="milb_recent_update_interval">Recent Update Interval (seconds):</label>
|
|
<input type="number" id="milb_recent_update_interval" name="milb_recent_update_interval" value="{{ main_config.milb.recent_update_interval }}" min="60" max="7200">
|
|
<div class="description">How often to update recent MiLB games</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="milb_upcoming_update_interval">Upcoming Update Interval (seconds):</label>
|
|
<input type="number" id="milb_upcoming_update_interval" name="milb_upcoming_update_interval" value="{{ main_config.milb.upcoming_update_interval }}" min="60" max="7200">
|
|
<div class="description">How often to update upcoming MiLB games</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="milb_recent_games_to_show">Recent Games to Show:</label>
|
|
<input type="number" id="milb_recent_games_to_show" name="milb_recent_games_to_show" value="{{ main_config.milb.recent_games_to_show }}" min="0" max="10">
|
|
<div class="description">Number of recent games to display</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="milb_upcoming_games_to_show">Upcoming Games to Show:</label>
|
|
<input type="number" id="milb_upcoming_games_to_show" name="milb_upcoming_games_to_show" value="{{ main_config.milb.upcoming_games_to_show }}" min="0" max="10">
|
|
<div class="description">Number of upcoming games to display</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="milb_show_records">Show Records:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="milb_show_records" name="milb_show_records" {% if main_config.milb.show_records %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Show team records</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="milb_upcoming_fetch_days">Upcoming Fetch Days:</label>
|
|
<input type="number" id="milb_upcoming_fetch_days" name="milb_upcoming_fetch_days" value="{{ main_config.milb.upcoming_fetch_days }}" min="1" max="30">
|
|
<div class="description">Number of days ahead to fetch upcoming games</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>Soccer</h3>
|
|
<div class="form-group">
|
|
<label for="soccer_enabled">Enable Soccer:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="soccer_enabled" name="soccer_enabled" {% if main_config.soccer_scoreboard.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="soccer_favorites">Favorite Teams:</label>
|
|
<div class="array-input">
|
|
<input type="text" id="soccer_favorites" name="soccer_favorites" value="{{ main_config.soccer_scoreboard.favorite_teams|join(', ') }}" placeholder="LIV">
|
|
<button type="button" class="add-item-btn" onclick="addArrayItem('soccer_favorites')">Add</button>
|
|
</div>
|
|
<div class="description">Comma-separated team abbreviations</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="soccer_leagues">Soccer Leagues:</label>
|
|
<div class="array-input">
|
|
<input type="text" id="soccer_leagues" name="soccer_leagues" value="{{ main_config.soccer_scoreboard.leagues|join(', ') }}" placeholder="eng.1, esp.1, ger.1">
|
|
<button type="button" class="add-item-btn" onclick="addArrayItem('soccer_leagues')">Add</button>
|
|
</div>
|
|
<div class="description">Comma-separated league codes (e.g., eng.1 for Premier League)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="soccer_live_priority">Live Priority:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="soccer_live_priority" name="soccer_live_priority" {% if main_config.soccer_scoreboard.live_priority %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Prioritize live/in-progress games over finished/upcoming games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="soccer_live_game_duration">Live Game Duration (minutes):</label>
|
|
<input type="number" id="soccer_live_game_duration" name="soccer_live_game_duration" value="{{ main_config.soccer_scoreboard.live_game_duration }}" min="5" max="300">
|
|
<div class="description">How long to display each live game</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="soccer_show_odds">Show Odds:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="soccer_show_odds" name="soccer_show_odds" {% if main_config.soccer_scoreboard.show_odds %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Display betting odds for games</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="soccer_recent_games_to_show">Recent Games to Show:</label>
|
|
<input type="number" id="soccer_recent_games_to_show" name="soccer_recent_games_to_show" value="{{ main_config.soccer_scoreboard.recent_games_to_show }}" min="1" max="10">
|
|
<div class="description">Number of most recent games to display (default: 5)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="soccer_upcoming_games_to_show">Upcoming Games to Show:</label>
|
|
<input type="number" id="soccer_upcoming_games_to_show" name="soccer_upcoming_games_to_show" value="{{ main_config.soccer_scoreboard.upcoming_games_to_show }}" min="1" max="10">
|
|
<div class="description">Number of upcoming games to display (default: 5)</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="soccer_show_favorite_teams_only">Show Favorite Teams Only:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="soccer_show_favorite_teams_only" name="soccer_show_favorite_teams_only" {% if main_config.soccer_scoreboard.show_favorite_teams_only %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Only display games involving your favorite teams</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="button" onclick="saveSportsConfig()">Save Sports Settings</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Weather Tab -->
|
|
<div id="weather" class="tab-content">
|
|
<div class="form-section">
|
|
<h2>Weather Configuration</h2>
|
|
<form action="{{ url_for('save_config_route') }}" method="POST" id="weather-form">
|
|
<input type="hidden" name="config_type" value="main">
|
|
<input type="hidden" name="config_data" id="weather-config-data">
|
|
|
|
<div class="form-group">
|
|
<label for="weather_enabled">Enable Weather:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="weather_enabled" name="weather_enabled" {% if main_config.weather.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="weather_city">City:</label>
|
|
<input type="text" id="weather_city" name="weather_city" value="{{ main_config.location.city }}">
|
|
<div class="description">City name for weather data</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="weather_state">State:</label>
|
|
<input type="text" id="weather_state" name="weather_state" value="{{ main_config.location.state }}">
|
|
<div class="description">State/province name</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="weather_units">Units:</label>
|
|
<select id="weather_units" name="weather_units">
|
|
<option value="imperial" {% if main_config.weather.units == "imperial" %}selected{% endif %}>Fahrenheit</option>
|
|
<option value="metric" {% if main_config.weather.units == "metric" %}selected{% endif %}>Celsius</option>
|
|
</select>
|
|
<div class="description">Temperature units</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="weather_update_interval">Update Interval (seconds):</label>
|
|
<input type="number" id="weather_update_interval" name="weather_update_interval" value="{{ main_config.weather.update_interval }}" min="300" max="3600">
|
|
<div class="description">How often to update weather data (300-3600 seconds)</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="weather_display_format">Display Format:</label>
|
|
<input type="text" id="weather_display_format" name="weather_display_format" value="{{ main_config.weather.display_format }}" placeholder="{temp}°F\n{condition}">
|
|
<div class="description">Weather display format (use {temp}, {condition}, {humidity}, etc.)</div>
|
|
</div>
|
|
|
|
<button type="submit">Save Weather Settings</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stocks Tab -->
|
|
<div id="stocks" class="tab-content">
|
|
<div class="form-section">
|
|
<h2>Stocks & Crypto Configuration</h2>
|
|
|
|
<div class="config-section">
|
|
<h3>Stocks</h3>
|
|
<form action="{{ url_for('save_config_route') }}" method="POST" id="stocks-form">
|
|
<input type="hidden" name="config_type" value="main">
|
|
<input type="hidden" name="config_data" id="stocks-config-data">
|
|
|
|
<div class="form-group">
|
|
<label for="stocks_enabled">Enable Stocks:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="stocks_enabled" name="stocks_enabled" {% if main_config.stocks.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="stocks_symbols">Stock Symbols:</label>
|
|
<div class="array-input">
|
|
<input type="text" id="stocks_symbols" name="stocks_symbols" value="{{ main_config.stocks.symbols|join(', ') }}" placeholder="AAPL, GOOGL, MSFT">
|
|
<button type="button" class="add-item-btn" onclick="addArrayItem('stocks_symbols')">Add</button>
|
|
</div>
|
|
<div class="description">Comma-separated stock symbols</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="stocks_update_interval">Update Interval (seconds):</label>
|
|
<input type="number" id="stocks_update_interval" name="stocks_update_interval" value="{{ main_config.stocks.update_interval }}" min="60" max="3600">
|
|
<div class="description">How often to update stock data</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="stocks_scroll_speed">Scroll Speed:</label>
|
|
<input type="number" id="stocks_scroll_speed" name="stocks_scroll_speed" value="{{ main_config.stocks.scroll_speed }}" min="1" max="10">
|
|
<div class="description">Scroll speed for stock ticker (1-10)</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="stocks_scroll_delay">Scroll Delay:</label>
|
|
<input type="number" id="stocks_scroll_delay" name="stocks_scroll_delay" value="{{ main_config.stocks.scroll_delay }}" min="0.01" max="1.0" step="0.01">
|
|
<div class="description">Scroll delay for stock ticker (0.01-1.0 seconds)</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="stocks_toggle_chart">Show Charts:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="stocks_toggle_chart" name="stocks_toggle_chart" {% if main_config.stocks.toggle_chart %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Display mini charts alongside stock ticker data</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="stocks_display_format">Display Format:</label>
|
|
<input type="text" id="stocks_display_format" name="stocks_display_format" value="{{ main_config.stocks.display_format }}" placeholder="{symbol}: ${price} ({change}%)">
|
|
<div class="description">Stock display format (use {symbol}, {price}, {change}, etc.)</div>
|
|
</div>
|
|
|
|
<button type="submit">Save Stocks Settings</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>Cryptocurrency</h3>
|
|
<form action="{{ url_for('save_config_route') }}" method="POST" id="crypto-form">
|
|
<input type="hidden" name="config_type" value="main">
|
|
<input type="hidden" name="config_data" id="crypto-config-data">
|
|
|
|
<div class="form-group">
|
|
<label for="crypto_enabled">Enable Crypto:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="crypto_enabled" name="crypto_enabled" {% if main_config.crypto.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="crypto_symbols">Crypto Symbols:</label>
|
|
<div class="array-input">
|
|
<input type="text" id="crypto_symbols" name="crypto_symbols" value="{{ main_config.crypto.symbols|join(', ') }}" placeholder="BTC-USD, ETH-USD">
|
|
<button type="button" class="add-item-btn" onclick="addArrayItem('crypto_symbols')">Add</button>
|
|
</div>
|
|
<div class="description">Comma-separated crypto symbols (e.g., BTC-USD, ETH-USD)</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="crypto_update_interval">Update Interval (seconds):</label>
|
|
<input type="number" id="crypto_update_interval" name="crypto_update_interval" value="{{ main_config.crypto.update_interval }}" min="60" max="3600">
|
|
<div class="description">How often to update crypto data</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="crypto_display_format">Display Format:</label>
|
|
<input type="text" id="crypto_display_format" name="crypto_display_format" value="{{ main_config.crypto.display_format }}" placeholder="{symbol}: ${price} ({change}%)">
|
|
<div class="description">Crypto display format (use {symbol}, {price}, {change}, etc.)</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="crypto_toggle_chart">Show Charts:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="crypto_toggle_chart" name="crypto_toggle_chart" {% if main_config.crypto.toggle_chart %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Display mini charts alongside crypto ticker data</div>
|
|
</div>
|
|
|
|
<button type="submit">Save Crypto Settings</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Additional Features Tab -->
|
|
<div id="features" class="tab-content">
|
|
<div class="form-section">
|
|
<h2>Additional Features</h2>
|
|
<p>Configure additional features like clock, stock news, odds ticker, YouTube, text display, and of the day.</p>
|
|
|
|
<div class="config-section">
|
|
<h3>Clock</h3>
|
|
<form action="{{ url_for('save_config_route') }}" method="POST" id="clock-form">
|
|
<input type="hidden" name="config_type" value="main">
|
|
<input type="hidden" name="config_data" id="clock-config-data">
|
|
|
|
<div class="form-group">
|
|
<label for="clock_enabled">Enable Clock:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="clock_enabled" name="clock_enabled" {% if main_config.clock.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="clock_format">Clock Format:</label>
|
|
<select id="clock_format" name="clock_format">
|
|
<option value="%I:%M %p" {% if main_config.clock.format == "%I:%M %p" %}selected{% endif %}>12-hour (AM/PM)</option>
|
|
<option value="%H:%M" {% if main_config.clock.format == "%H:%M" %}selected{% endif %}>24-hour</option>
|
|
</select>
|
|
<div class="description">Time format for the clock display</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="clock_update_interval">Update Interval (seconds):</label>
|
|
<input type="number" id="clock_update_interval" name="clock_update_interval" value="{{ main_config.clock.update_interval }}" min="1" max="60">
|
|
<div class="description">How often to update the clock display</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="clock_date_format">Date Format:</label>
|
|
<select id="clock_date_format" name="clock_date_format">
|
|
<option value="MM/DD/YYYY" {% if main_config.clock.date_format == "MM/DD/YYYY" %}selected{% endif %}>MM/DD/YYYY</option>
|
|
<option value="DD/MM/YYYY" {% if main_config.clock.date_format == "DD/MM/YYYY" %}selected{% endif %}>DD/MM/YYYY</option>
|
|
<option value="YYYY/MM/DD" {% if main_config.clock.date_format == "YYYY/MM/DD" %}selected{% endif %}>YYYY/MM/DD</option>
|
|
</select>
|
|
<div class="description">Date format for the clock display</div>
|
|
</div>
|
|
|
|
<button type="submit">Save Clock Settings</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>Stock News</h3>
|
|
<form action="{{ url_for('save_config_route') }}" method="POST" id="stock-news-form">
|
|
<input type="hidden" name="config_type" value="main">
|
|
<input type="hidden" name="config_data" id="stock-news-config-data">
|
|
|
|
<div class="form-group">
|
|
<label for="stock_news_enabled">Enable Stock News:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="stock_news_enabled" name="stock_news_enabled" {% if main_config.stock_news.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="stock_news_update_interval">Update Interval (seconds):</label>
|
|
<input type="number" id="stock_news_update_interval" name="stock_news_update_interval" value="{{ main_config.stock_news.update_interval }}" min="60" max="3600">
|
|
<div class="description">How often to update stock news</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="stock_news_scroll_speed">Scroll Speed:</label>
|
|
<input type="number" id="stock_news_scroll_speed" name="stock_news_scroll_speed" value="{{ main_config.stock_news.scroll_speed }}" min="1" max="10">
|
|
<div class="description">Scroll speed for stock news (1-10)</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="stock_news_scroll_delay">Scroll Delay:</label>
|
|
<input type="number" id="stock_news_scroll_delay" name="stock_news_scroll_delay" value="{{ main_config.stock_news.scroll_delay }}" min="0.01" max="1.0" step="0.01">
|
|
<div class="description">Scroll delay for stock news (0.01-1.0 seconds)</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="stock_news_max_headlines_per_symbol">Max Headlines Per Symbol:</label>
|
|
<input type="number" id="stock_news_max_headlines_per_symbol" name="stock_news_max_headlines_per_symbol" value="{{ main_config.stock_news.max_headlines_per_symbol }}" min="1" max="10">
|
|
<div class="description">Maximum headlines to show per stock symbol</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="stock_news_headlines_per_rotation">Headlines Per Rotation:</label>
|
|
<input type="number" id="stock_news_headlines_per_rotation" name="stock_news_headlines_per_rotation" value="{{ main_config.stock_news.headlines_per_rotation }}" min="1" max="20">
|
|
<div class="description">Number of headlines to show per rotation</div>
|
|
</div>
|
|
|
|
<button type="submit">Save Stock News Settings</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>Odds Ticker</h3>
|
|
<form action="{{ url_for('save_config_route') }}" method="POST" id="odds-ticker-form">
|
|
<input type="hidden" name="config_type" value="main">
|
|
<input type="hidden" name="config_data" id="odds-ticker-config-data">
|
|
|
|
<div class="form-group">
|
|
<label for="odds_ticker_enabled">Enable Odds Ticker:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="odds_ticker_enabled" name="odds_ticker_enabled" {% if main_config.odds_ticker.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="odds_ticker_update_interval">Update Interval (seconds):</label>
|
|
<input type="number" id="odds_ticker_update_interval" name="odds_ticker_update_interval" value="{{ main_config.odds_ticker.update_interval }}" min="60" max="3600">
|
|
<div class="description">How often to update odds</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="odds_ticker_show_favorite_teams_only">Show Favorite Teams Only:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="odds_ticker_show_favorite_teams_only" name="odds_ticker_show_favorite_teams_only" {% if main_config.odds_ticker.show_favorite_teams_only %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Only show odds for favorite teams</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="odds_ticker_games_per_favorite_team">Games Per Favorite Team:</label>
|
|
<input type="number" id="odds_ticker_games_per_favorite_team" name="odds_ticker_games_per_favorite_team" value="{{ main_config.odds_ticker.games_per_favorite_team }}" min="1" max="10">
|
|
<div class="description">Number of games to show per favorite team</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="odds_ticker_max_games_per_league">Max Games Per League:</label>
|
|
<input type="number" id="odds_ticker_max_games_per_league" name="odds_ticker_max_games_per_league" value="{{ main_config.odds_ticker.max_games_per_league }}" min="1" max="20">
|
|
<div class="description">Maximum games to show per league</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="odds_ticker_show_odds_only">Show Odds Only:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="odds_ticker_show_odds_only" name="odds_ticker_show_odds_only" {% if main_config.odds_ticker.show_odds_only %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Show only odds without game details</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="odds_ticker_sort_order">Sort Order:</label>
|
|
<select id="odds_ticker_sort_order" name="odds_ticker_sort_order">
|
|
<option value="soonest" {% if main_config.odds_ticker.sort_order == "soonest" %}selected{% endif %}>Soonest</option>
|
|
<option value="latest" {% if main_config.odds_ticker.sort_order == "latest" %}selected{% endif %}>Latest</option>
|
|
<option value="alphabetical" {% if main_config.odds_ticker.sort_order == "alphabetical" %}selected{% endif %}>Alphabetical</option>
|
|
</select>
|
|
<div class="description">How to sort the odds ticker</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="odds_ticker_enabled_leagues">Enabled Leagues:</label>
|
|
<div class="array-input">
|
|
<input type="text" id="odds_ticker_enabled_leagues" name="odds_ticker_enabled_leagues" value="{{ main_config.odds_ticker.enabled_leagues|join(', ') }}" placeholder="nfl, mlb, ncaa_fb, milb">
|
|
<button type="button" class="add-item-btn" onclick="addArrayItem('odds_ticker_enabled_leagues')">Add</button>
|
|
</div>
|
|
<div class="description">Comma-separated list of enabled leagues</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="odds_ticker_scroll_speed">Scroll Speed:</label>
|
|
<input type="number" id="odds_ticker_scroll_speed" name="odds_ticker_scroll_speed" value="{{ main_config.odds_ticker.scroll_speed }}" min="1" max="10">
|
|
<div class="description">Scroll speed for odds ticker (1-10)</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="odds_ticker_scroll_delay">Scroll Delay:</label>
|
|
<input type="number" id="odds_ticker_scroll_delay" name="odds_ticker_scroll_delay" value="{{ main_config.odds_ticker.scroll_delay }}" min="0.01" max="1.0" step="0.01">
|
|
<div class="description">Scroll delay for odds ticker (0.01-1.0 seconds)</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="odds_ticker_loop">Loop:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="odds_ticker_loop" name="odds_ticker_loop" {% if main_config.odds_ticker.loop %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Loop the odds ticker continuously</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="odds_ticker_future_fetch_days">Future Fetch Days:</label>
|
|
<input type="number" id="odds_ticker_future_fetch_days" name="odds_ticker_future_fetch_days" value="{{ main_config.odds_ticker.future_fetch_days }}" min="1" max="365">
|
|
<div class="description">Number of days ahead to fetch odds for</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="odds_ticker_show_channel_logos">Show Channel Logos:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="odds_ticker_show_channel_logos" name="odds_ticker_show_channel_logos" {% if main_config.odds_ticker.show_channel_logos %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Show broadcast channel logos</div>
|
|
</div>
|
|
|
|
<button type="submit">Save Odds Ticker Settings</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>YouTube</h3>
|
|
<form action="{{ url_for('save_config_route') }}" method="POST" id="youtube-form">
|
|
<input type="hidden" name="config_type" value="main">
|
|
<input type="hidden" name="config_data" id="youtube-config-data">
|
|
|
|
<div class="form-group">
|
|
<label for="youtube_enabled">Enable YouTube:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="youtube_enabled" name="youtube_enabled" {% if main_config.youtube.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="youtube_channel_id">YouTube Channel ID:</label>
|
|
<input type="text" id="youtube_channel_id" name="youtube_channel_id" value="{{ main_config.youtube.channel_id if main_config.youtube.channel_id else '' }}" placeholder="Enter your YouTube channel ID">
|
|
<div class="description">Your YouTube channel ID (found in channel settings)</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="youtube_update_interval">Update Interval (seconds):</label>
|
|
<input type="number" id="youtube_update_interval" name="youtube_update_interval" value="{{ main_config.youtube.update_interval }}" min="60" max="3600">
|
|
<div class="description">How often to update YouTube info</div>
|
|
</div>
|
|
|
|
<button type="submit">Save YouTube Settings</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>Text Display</h3>
|
|
<form action="{{ url_for('save_config_route') }}" method="POST" id="text-display-form">
|
|
<input type="hidden" name="config_type" value="main">
|
|
<input type="hidden" name="config_data" id="text-display-config-data">
|
|
|
|
<div class="form-group">
|
|
<label for="text_display_enabled">Enable Custom Text Display:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="text_display_enabled" name="text_display_enabled" {% if main_config.text_display.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="text_display_text">Custom Text:</label>
|
|
<textarea id="text_display_text" name="text_display_text" rows="5" cols="50">{{ main_config.text_display.text }}</textarea>
|
|
<div class="description">Custom text to display on the LED matrix</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="text_display_font_path">Font Path:</label>
|
|
<input type="text" id="text_display_font_path" name="text_display_font_path" value="{{ main_config.text_display.font_path }}" placeholder="assets/fonts/press-start-2p.ttf">
|
|
<div class="description">Path to the font file for text display</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="text_display_font_size">Font Size:</label>
|
|
<input type="number" id="text_display_font_size" name="text_display_font_size" value="{{ main_config.text_display.font_size }}" min="1" max="20">
|
|
<div class="description">Font size for text display (1-20)</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="text_display_scroll">Scroll Text:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="text_display_scroll" name="text_display_scroll" {% if main_config.text_display.scroll %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Enable text scrolling</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="text_display_scroll_speed">Scroll Speed:</label>
|
|
<input type="number" id="text_display_scroll_speed" name="text_display_scroll_speed" value="{{ main_config.text_display.scroll_speed }}" min="1" max="100">
|
|
<div class="description">Scroll speed for text (1-100)</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="text_display_text_color">Text Color (RGB):</label>
|
|
<input type="text" id="text_display_text_color" name="text_display_text_color" value="{{ main_config.text_display.text_color|join(', ') }}" placeholder="255, 0, 0">
|
|
<div class="description">Text color as RGB values (0-255, comma-separated)</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="text_display_background_color">Background Color (RGB):</label>
|
|
<input type="text" id="text_display_background_color" name="text_display_background_color" value="{{ main_config.text_display.background_color|join(', ') }}" placeholder="0, 0, 0">
|
|
<div class="description">Background color as RGB values (0-255, comma-separated)</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="text_display_scroll_gap_width">Scroll Gap Width:</label>
|
|
<input type="number" id="text_display_scroll_gap_width" name="text_display_scroll_gap_width" value="{{ main_config.text_display.scroll_gap_width }}" min="0" max="100">
|
|
<div class="description">Gap width for scrolling text (0-100 pixels)</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="text_display_duration">Text Display Duration (seconds):</label>
|
|
<input type="number" id="text_display_duration" name="text_display_duration" value="{{ main_config.display.display_durations.text_display }}" min="5" max="120">
|
|
<div class="description">How long to show custom text</div>
|
|
</div>
|
|
|
|
<button type="submit">Save Text Display Settings</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>Of The Day</h3>
|
|
<form action="{{ url_for('save_config_route') }}" method="POST" id="of_the_day-form">
|
|
<input type="hidden" name="config_type" value="main">
|
|
<input type="hidden" name="config_data" id="of_the_day-config-data">
|
|
|
|
<div class="form-group">
|
|
<label for="of_the_day_enabled">Enable Of The Day:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="of_the_day_enabled" name="of_the_day_enabled" {% if main_config.of_the_day.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="of_the_day_update_interval">Update Interval (seconds):</label>
|
|
<input type="number" id="of_the_day_update_interval" name="of_the_day_update_interval" value="{{ main_config.of_the_day.update_interval }}" min="300" max="3600">
|
|
<div class="description">How often to update word of the day</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="of_the_day_display_rotate_interval">Display Rotate Interval (seconds):</label>
|
|
<input type="number" id="of_the_day_display_rotate_interval" name="of_the_day_display_rotate_interval" value="{{ main_config.of_the_day.display_rotate_interval }}" min="5" max="120">
|
|
<div class="description">How often to rotate between different 'of the day' items</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="of_the_day_subtitle_rotate_interval">Subtitle Rotate Interval (seconds):</label>
|
|
<input type="number" id="of_the_day_subtitle_rotate_interval" name="of_the_day_subtitle_rotate_interval" value="{{ main_config.of_the_day.subtitle_rotate_interval }}" min="1" max="60">
|
|
<div class="description">How often to rotate subtitles</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="of_the_day_category_order">Category Order:</label>
|
|
<div class="array-input">
|
|
<input type="text" id="of_the_day_category_order" name="of_the_day_category_order" value="{{ main_config.of_the_day.category_order|join(', ') }}" placeholder="word_of_the_day, slovenian_word_of_the_day">
|
|
<button type="button" class="add-item-btn" onclick="addArrayItem('of_the_day_category_order')">Add</button>
|
|
</div>
|
|
<div class="description">Order of categories to display</div>
|
|
</div>
|
|
|
|
<button type="submit">Save Of The Day Settings</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Music Tab -->
|
|
<div id="music" class="tab-content">
|
|
<div class="form-section">
|
|
<h2>Music Configuration</h2>
|
|
<form action="{{ url_for('save_config_route') }}" method="POST" id="music-form">
|
|
<input type="hidden" name="config_type" value="main">
|
|
<input type="hidden" name="config_data" id="music-config-data">
|
|
|
|
<div class="form-group">
|
|
<label for="music_enabled">Enable Music Display:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="music_enabled" name="music_enabled" {% if main_config.music.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="music_preferred_source">Preferred Source:</label>
|
|
<select id="music_preferred_source" name="music_preferred_source">
|
|
<option value="ytm" {% if main_config.music.preferred_source == "ytm" %}selected{% endif %}>YouTube Music</option>
|
|
<option value="spotify" {% if main_config.music.preferred_source == "spotify" %}selected{% endif %}>Spotify</option>
|
|
</select>
|
|
<div class="description">Primary music source to display</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="ytm_companion_url">YouTube Music Companion URL:</label>
|
|
<input type="text" id="ytm_companion_url" name="ytm_companion_url" value="{{ main_config.music.YTM_COMPANION_URL }}">
|
|
<div class="description">URL for YouTube Music companion app</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="music_polling_interval">Polling Interval (seconds):</label>
|
|
<input type="number" id="music_polling_interval" name="music_polling_interval" value="{{ main_config.music.POLLING_INTERVAL_SECONDS }}" min="1" max="60">
|
|
<div class="description">How often to check for music updates</div>
|
|
</div>
|
|
|
|
<button type="submit">Save Music Settings</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Calendar Tab -->
|
|
<div id="calendar" class="tab-content">
|
|
<div class="form-section">
|
|
<h2>Calendar Configuration</h2>
|
|
<form action="{{ url_for('save_config_route') }}" method="POST" id="calendar-form">
|
|
<input type="hidden" name="config_type" value="main">
|
|
<input type="hidden" name="config_data" id="calendar-config-data">
|
|
|
|
<div class="form-group">
|
|
<label for="calendar_enabled">Enable Calendar:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="calendar_enabled" name="calendar_enabled" {% if main_config.calendar.enabled %}checked{% endif %}>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="calendar_max_events">Max Events to Show:</label>
|
|
<input type="number" id="calendar_max_events" name="calendar_max_events" value="{{ main_config.calendar.max_events }}" min="1" max="10">
|
|
<div class="description">Maximum number of events to display</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="calendar_update_interval">Update Interval (seconds):</label>
|
|
<input type="number" id="calendar_update_interval" name="calendar_update_interval" value="{{ main_config.calendar.update_interval }}" min="300" max="3600">
|
|
<div class="description">How often to update calendar data</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="calendar_calendars">Calendars:</label>
|
|
<div class="array-input">
|
|
<input type="text" id="calendar_calendars" name="calendar_calendars" value="{{ main_config.calendar.calendars|join(', ') }}" placeholder="birthdays, work">
|
|
<button type="button" class="add-item-btn" onclick="addArrayItem('calendar_calendars')">Add</button>
|
|
</div>
|
|
<div class="description">Comma-separated calendar names</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="calendar_credentials_file">Credentials File:</label>
|
|
<input type="text" id="calendar_credentials_file" name="calendar_credentials_file" value="{{ main_config.calendar.credentials_file }}" placeholder="credentials.json">
|
|
<div class="description">Path to Google Calendar credentials file</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="calendar_token_file">Token File:</label>
|
|
<input type="text" id="calendar_token_file" name="calendar_token_file" value="{{ main_config.calendar.token_file }}" placeholder="token.pickle">
|
|
<div class="description">Path to Google Calendar token file</div>
|
|
</div>
|
|
|
|
<button type="submit">Save Calendar Settings</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- News Manager Tab -->
|
|
<div id="news" class="tab-content">
|
|
<div class="form-section">
|
|
<h2>News Manager Configuration</h2>
|
|
<p>Configure RSS news feeds and scrolling ticker settings</p>
|
|
|
|
<div class="form-group">
|
|
<label for="news_enabled">Enable News Manager:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="news_enabled" name="news_enabled">
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="headlines_per_feed">Headlines Per Feed:</label>
|
|
<input type="number" id="headlines_per_feed" name="headlines_per_feed" min="1" max="5" value="2">
|
|
<div class="description">Number of headlines to show from each enabled feed</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Available News Feeds:</label>
|
|
<div class="checkbox-grid" id="news_feeds_grid">
|
|
<!-- Feeds will be populated by JavaScript -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<h3>Custom RSS Feeds</h3>
|
|
<div class="custom-feeds-container">
|
|
<div class="add-custom-feed">
|
|
<input type="text" id="custom_feed_name" placeholder="Feed Name" style="width: 200px;">
|
|
<input type="text" id="custom_feed_url" placeholder="RSS Feed URL" style="width: 400px;">
|
|
<button type="button" onclick="addCustomFeed()">Add Feed</button>
|
|
</div>
|
|
<div id="custom_feeds_list">
|
|
<!-- Custom feeds will be populated by JavaScript -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<h3>Scrolling Settings</h3>
|
|
<div class="settings-row">
|
|
<div>
|
|
<label for="scroll_speed">Scroll Speed:</label>
|
|
<input type="number" id="scroll_speed" min="1" max="10" value="2">
|
|
<div class="description">Pixels per frame</div>
|
|
</div>
|
|
<div>
|
|
<label for="scroll_delay">Scroll Delay (ms):</label>
|
|
<input type="number" id="scroll_delay" min="5" max="100" value="20">
|
|
<div class="description">Delay between scroll updates</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="dynamic_duration">Enable Dynamic Duration:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="dynamic_duration" name="dynamic_duration" checked>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Automatically calculate display time based on headline length</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<h3>Duration Settings</h3>
|
|
<div class="settings-row">
|
|
<div>
|
|
<label for="min_duration">Min Duration (s):</label>
|
|
<input type="number" id="min_duration" min="10" max="120" value="30">
|
|
<div class="description">Minimum display time</div>
|
|
</div>
|
|
<div>
|
|
<label for="max_duration">Max Duration (s):</label>
|
|
<input type="number" id="max_duration" min="60" max="600" value="300">
|
|
<div class="description">Maximum display time</div>
|
|
</div>
|
|
<div>
|
|
<label for="duration_buffer">Buffer (%):</label>
|
|
<input type="number" id="duration_buffer" min="0" max="50" value="10" step="5">
|
|
<div class="description">Extra time for smooth cycling</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="rotation_enabled">Enable Headline Rotation:</label>
|
|
<div class="toggle-container">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="rotation_enabled" name="rotation_enabled" checked>
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="description">Rotate through different headlines to avoid repetition</div>
|
|
</div>
|
|
|
|
<div class="button-group">
|
|
<button type="button" onclick="saveNewsSettings()">Save News Settings</button>
|
|
<button type="button" onclick="refreshNewsStatus()">Refresh Status</button>
|
|
</div>
|
|
|
|
<div id="news_status" class="status-container" style="margin-top: 20px;">
|
|
<!-- Status will be populated by JavaScript -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Secrets Tab -->
|
|
<div id="secrets" class="tab-content">
|
|
<div class="form-section">
|
|
<h2>API Keys Configuration</h2>
|
|
<p>Enter your API keys for various services. These are stored securely and not shared.</p>
|
|
|
|
<div id="secrets-form" class="form-editor">
|
|
<form action="{{ url_for('save_config_route') }}" method="POST" id="secrets-form-content">
|
|
<input type="hidden" name="config_type" value="secrets">
|
|
<input type="hidden" name="config_data" id="secrets-config-data">
|
|
|
|
<div class="config-section">
|
|
<h3>Weather API</h3>
|
|
<div class="form-group">
|
|
<label for="weather_api_key">OpenWeatherMap API Key:</label>
|
|
<input type="password" id="weather_api_key" name="weather_api_key" value="{{ secrets_config.weather.api_key if secrets_config.weather.api_key != 'YOUR_OPENWEATHERMAP_API_KEY' else '' }}" placeholder="Enter your OpenWeatherMap API key">
|
|
<div class="description">Get your free API key from <a href="https://openweathermap.org/api" target="_blank">OpenWeatherMap</a></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>YouTube API</h3>
|
|
<div class="form-group">
|
|
<label for="youtube_api_key">YouTube API Key:</label>
|
|
<input type="password" id="youtube_api_key" name="youtube_api_key" value="{{ secrets_config.youtube.api_key if secrets_config.youtube.api_key != 'YOUR_YOUTUBE_API_KEY' else '' }}" placeholder="Enter your YouTube API key">
|
|
<div class="description">Get your API key from <a href="https://console.developers.google.com/" target="_blank">Google Cloud Console</a></div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="youtube_channel_id">YouTube Channel ID:</label>
|
|
<input type="text" id="youtube_channel_id" name="youtube_channel_id" value="{{ secrets_config.youtube.channel_id if secrets_config.youtube.channel_id != 'YOUR_YOUTUBE_CHANNEL_ID' else '' }}" placeholder="Enter your YouTube channel ID">
|
|
<div class="description">Your YouTube channel ID (found in channel settings)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>Spotify API</h3>
|
|
<div class="form-group">
|
|
<label for="spotify_client_id">Spotify Client ID:</label>
|
|
<input type="password" id="spotify_client_id" name="spotify_client_id" value="{{ secrets_config.music.SPOTIFY_CLIENT_ID if secrets_config.music.SPOTIFY_CLIENT_ID != 'YOUR_SPOTIFY_CLIENT_ID_HERE' else '' }}" placeholder="Enter your Spotify Client ID">
|
|
<div class="description">Get from <a href="https://developer.spotify.com/dashboard" target="_blank">Spotify Developer Dashboard</a></div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="spotify_client_secret">Spotify Client Secret:</label>
|
|
<input type="password" id="spotify_client_secret" name="spotify_client_secret" value="{{ secrets_config.music.SPOTIFY_CLIENT_SECRET if secrets_config.music.SPOTIFY_CLIENT_SECRET != 'YOUR_SPOTIFY_CLIENT_SECRET_HERE' else '' }}" placeholder="Enter your Spotify Client Secret">
|
|
<div class="description">Your Spotify Client Secret</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="spotify_redirect_uri">Spotify Redirect URI:</label>
|
|
<input type="text" id="spotify_redirect_uri" name="spotify_redirect_uri" value="{{ secrets_config.music.SPOTIFY_REDIRECT_URI }}" placeholder="http://127.0.0.1:8888/callback">
|
|
<div class="description">Redirect URI for Spotify authentication</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit">Save API Keys</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div id="secrets-json" class="json-editor">
|
|
<form action="{{ url_for('save_config_route') }}" method="POST">
|
|
<input type="hidden" name="config_type" value="secrets">
|
|
<h2>Secrets Configuration (<span class="filepath">{{ secrets_config_path }}</span>)</h2>
|
|
<textarea name="config_data">{{ secrets_config_json }}</textarea>
|
|
<button type="submit">Save Secrets</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions Tab -->
|
|
<div id="actions" class="tab-content">
|
|
<div class="form-section">
|
|
<h2>System Actions</h2>
|
|
<p>Control the display service and system operations.</p>
|
|
|
|
<div class="config-section">
|
|
<h3>Display Control</h3>
|
|
<div class="action-buttons">
|
|
<button type="button" class="action-button" onclick="runAction('start_display')">Start Display</button>
|
|
<button type="button" class="action-button" onclick="runAction('stop_display')">Stop Display</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>Auto-Start Settings</h3>
|
|
<div class="action-buttons">
|
|
<button type="button" class="action-button" onclick="runAction('enable_autostart')">Enable Auto-Start</button>
|
|
<button type="button" class="action-button" onclick="runAction('disable_autostart')">Disable Auto-Start</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>System Operations</h3>
|
|
<div class="action-buttons">
|
|
<button type="button" class="action-button" onclick="runAction('reboot_system')">Reboot System</button>
|
|
<button type="button" class="action-button" onclick="runAction('git_pull')">Download Latest Update</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>Action Output</h3>
|
|
<div id="action_output_container" style="margin-top: 20px;">
|
|
<pre id="action_output">No action run yet.</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Raw JSON Tab -->
|
|
<div id="raw-json" class="tab-content">
|
|
<div class="form-section">
|
|
<h2>Raw Configuration JSON</h2>
|
|
<p>View, edit, and save the complete configuration files directly. <strong>⚠️ Warning:</strong> Be careful when editing raw JSON - invalid syntax will prevent saving. Use the "Validate JSON" button to check your changes before saving.</p>
|
|
|
|
<div class="config-section">
|
|
<h3>Main Configuration (config.json)</h3>
|
|
<div class="form-group">
|
|
<label>Configuration Path:</label>
|
|
<span>{{ main_config_path }}</span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Raw JSON:</label>
|
|
<div class="json-container">
|
|
<textarea id="main-config-json" style="height: 400px; font-family: monospace; font-size: 12px;">{{ main_config_json }}</textarea>
|
|
<div id="main-config-status" class="json-status valid">VALID</div>
|
|
<div id="main-config-validation" class="json-validation"></div>
|
|
</div>
|
|
</div>
|
|
<div class="json-actions">
|
|
<button type="button" onclick="copyToClipboard('main-config-json')" style="margin-right: 10px;">Copy Main Config</button>
|
|
<button type="button" onclick="formatJson('main-config-json')" style="background-color: #FF9800; margin-right: 10px;">Format JSON</button>
|
|
<button type="button" onclick="validateJson('main-config-json', 'main-config-validation')" style="background-color: #2196F3;">Validate JSON</button>
|
|
<button type="button" onclick="saveRawJson('main')" class="save-button" style="margin-left: 10px;">Save Main Config</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="config-section">
|
|
<h3>Secrets Configuration (config_secrets.json)</h3>
|
|
<div class="form-group">
|
|
<label>Configuration Path:</label>
|
|
<span>{{ secrets_config_path }}</span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Raw JSON:</label>
|
|
<div class="json-container">
|
|
<textarea id="secrets-config-json" style="height: 400px; font-family: monospace; font-size: 12px;">{{ secrets_config_json }}</textarea>
|
|
<div id="secrets-config-status" class="json-status valid">VALID</div>
|
|
<div id="secrets-config-validation" class="json-validation"></div>
|
|
</div>
|
|
</div>
|
|
<div class="json-actions">
|
|
<button type="button" onclick="copyToClipboard('secrets-config-json')" style="margin-right: 10px;">Copy Secrets Config</button>
|
|
<button type="button" onclick="formatJson('secrets-config-json')" style="background-color: #FF9800; margin-right: 10px;">Format JSON</button>
|
|
<button type="button" onclick="validateJson('secrets-config-json', 'secrets-config-validation')" style="background-color: #2196F3;">Validate JSON</button>
|
|
<button type="button" onclick="previewChanges('secrets')" style="background-color: #9C27B0; margin-right: 10px;">Preview Changes</button>
|
|
<button type="button" onclick="saveRawJson('secrets')" class="save-button" style="margin-left: 10px;">Save Secrets Config</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Logs Tab -->
|
|
<div id="logs" class="tab-content">
|
|
<div class="form-section">
|
|
<h2>System Logs</h2>
|
|
<p>View logs for the LED matrix service. Useful for debugging.</p>
|
|
<button id="refresh-logs-btn">Refresh Logs</button>
|
|
<pre id="log-content" style="background-color: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 5px; max-height: 600px; overflow-y: auto; white-space: pre-wrap; word-wrap: break-word;"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function openTab(evt, tabName) {
|
|
var i, tabcontent, tablinks;
|
|
tabcontent = document.getElementsByClassName("tab-content");
|
|
for (i = 0; i < tabcontent.length; i++) {
|
|
tabcontent[i].style.display = "none";
|
|
}
|
|
tablinks = document.getElementsByClassName("tab-link");
|
|
for (i = 0; i < tablinks.length; i++) {
|
|
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
|
}
|
|
document.getElementById(tabName).style.display = "block";
|
|
evt.currentTarget.className += " active";
|
|
|
|
// Auto-validate JSON when raw-json tab is opened
|
|
if (tabName === 'raw-json') {
|
|
// Small delay to ensure the tab content is rendered
|
|
setTimeout(() => {
|
|
validateJson('main-config-json', 'main-config-validation');
|
|
validateJson('secrets-config-json', 'secrets-config-validation');
|
|
}, 100);
|
|
}
|
|
if (tabName === 'logs') {
|
|
fetchLogs();
|
|
}
|
|
}
|
|
|
|
function toggleJsonEditor(section) {
|
|
const formEditor = document.getElementById(section + '-form');
|
|
const jsonEditor = document.getElementById(section + '-json');
|
|
|
|
if (formEditor.style.display === 'none') {
|
|
formEditor.style.display = 'block';
|
|
jsonEditor.style.display = 'none';
|
|
} else {
|
|
formEditor.style.display = 'none';
|
|
jsonEditor.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function addArrayItem(inputId) {
|
|
const input = document.getElementById(inputId);
|
|
const currentValue = input.value.trim();
|
|
if (currentValue && !currentValue.endsWith(',')) {
|
|
input.value = currentValue + ', ';
|
|
}
|
|
input.focus();
|
|
}
|
|
|
|
function copyToClipboard(elementId) {
|
|
const textarea = document.getElementById(elementId);
|
|
textarea.select();
|
|
textarea.setSelectionRange(0, 99999); // For mobile devices
|
|
|
|
try {
|
|
document.execCommand('copy');
|
|
alert('Configuration copied to clipboard!');
|
|
} catch (err) {
|
|
// Fallback for modern browsers
|
|
navigator.clipboard.writeText(textarea.value).then(function() {
|
|
alert('Configuration copied to clipboard!');
|
|
}).catch(function(err) {
|
|
alert('Failed to copy: ' + err);
|
|
});
|
|
}
|
|
}
|
|
|
|
function formatJson(elementId) {
|
|
const textarea = document.getElementById(elementId);
|
|
const jsonText = textarea.value;
|
|
|
|
try {
|
|
// Parse and re-stringify with proper formatting
|
|
const parsed = JSON.parse(jsonText);
|
|
const formatted = JSON.stringify(parsed, null, 4);
|
|
textarea.value = formatted;
|
|
|
|
// Update visual state
|
|
textarea.classList.remove('error');
|
|
textarea.classList.add('valid');
|
|
|
|
showMessage('JSON formatted successfully!', 'success');
|
|
} catch (error) {
|
|
showMessage(`Cannot format invalid JSON: ${error.message}`, 'error');
|
|
textarea.classList.remove('valid');
|
|
textarea.classList.add('error');
|
|
}
|
|
}
|
|
|
|
function previewChanges(configType) {
|
|
const textareaId = configType === 'main' ? 'main-config-json' : 'secrets-config-json';
|
|
const textarea = document.getElementById(textareaId);
|
|
const jsonText = textarea.value;
|
|
|
|
try {
|
|
// Validate JSON first
|
|
const parsed = JSON.parse(jsonText);
|
|
|
|
// Show a simple preview of the structure
|
|
const configName = configType === 'main' ? 'Main Configuration' : 'Secrets Configuration';
|
|
const previewText = `Preview of ${configName}:\n\n` +
|
|
`Top-level keys: ${Object.keys(parsed).join(', ')}\n\n` +
|
|
`Total size: ${JSON.stringify(parsed).length} characters\n\n` +
|
|
`This will overwrite the current ${configType === 'main' ? 'config.json' : 'config_secrets.json'} file.`;
|
|
|
|
alert(previewText);
|
|
} catch (error) {
|
|
showMessage(`Cannot preview invalid JSON: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
function validateJson(textareaId, validationId) {
|
|
const textarea = document.getElementById(textareaId);
|
|
const validationDiv = document.getElementById(validationId);
|
|
const jsonText = textarea.value;
|
|
|
|
// Clear previous validation
|
|
validationDiv.innerHTML = '';
|
|
validationDiv.className = 'json-validation';
|
|
validationDiv.style.display = 'block';
|
|
|
|
// Update status indicator
|
|
const statusId = validationId.replace('-validation', '-status');
|
|
const statusElement = document.getElementById(statusId);
|
|
|
|
try {
|
|
// Try to parse the JSON
|
|
const parsed = JSON.parse(jsonText);
|
|
|
|
// Check for common issues
|
|
const warnings = [];
|
|
const issues = [];
|
|
|
|
// Check if it's an empty object
|
|
if (Object.keys(parsed).length === 0) {
|
|
warnings.push('JSON is an empty object');
|
|
}
|
|
|
|
// Check for potential issues in nested objects
|
|
function checkObject(obj, path = '') {
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
const currentPath = path ? `${path}.${key}` : key;
|
|
|
|
// Check for empty strings in important fields
|
|
if (typeof value === 'string' && value.trim() === '' &&
|
|
(key.includes('api_key') || key.includes('client_id') || key.includes('secret'))) {
|
|
warnings.push(`Empty value in sensitive field: ${currentPath}`);
|
|
}
|
|
|
|
// Check for nested objects
|
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
checkObject(value, currentPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
checkObject(parsed);
|
|
|
|
// Display results
|
|
if (warnings.length > 0) {
|
|
validationDiv.className = 'json-validation warning';
|
|
if (statusElement) {
|
|
statusElement.textContent = 'WARNING';
|
|
statusElement.className = 'json-status warning';
|
|
}
|
|
validationDiv.innerHTML = `
|
|
<div class="json-error-message">✅ JSON is valid but has warnings:</div>
|
|
<div class="json-error-details">
|
|
${warnings.map(warning => `<div>⚠️ ${warning}</div>`).join('')}
|
|
</div>
|
|
`;
|
|
} else {
|
|
validationDiv.className = 'json-validation success';
|
|
if (statusElement) {
|
|
statusElement.textContent = 'VALID';
|
|
statusElement.className = 'json-status valid';
|
|
}
|
|
validationDiv.innerHTML = `
|
|
<div class="json-error-message">✅ JSON is valid!</div>
|
|
<div class="json-error-details">
|
|
✓ Valid JSON syntax<br>
|
|
✓ Proper structure<br>
|
|
✓ No obvious issues detected
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
} catch (error) {
|
|
// JSON parsing failed
|
|
validationDiv.className = 'json-validation error';
|
|
if (statusElement) {
|
|
statusElement.textContent = 'INVALID';
|
|
statusElement.className = 'json-status error';
|
|
}
|
|
|
|
let errorMessage = '❌ Invalid JSON syntax';
|
|
let errorDetails = error.message;
|
|
|
|
// Try to provide more helpful error messages
|
|
if (error.message.includes('Unexpected token')) {
|
|
errorMessage = '❌ JSON syntax error';
|
|
errorDetails = 'Check for missing commas, brackets, or quotes';
|
|
} else if (error.message.includes('Unexpected end')) {
|
|
errorMessage = '❌ Incomplete JSON';
|
|
errorDetails = 'JSON appears to be cut off or missing closing brackets/braces';
|
|
} else if (error.message.includes('Unexpected number')) {
|
|
errorMessage = '❌ Invalid number format';
|
|
errorDetails = 'Check for invalid number values';
|
|
}
|
|
|
|
// Try to find the line number
|
|
const lines = jsonText.split('\n');
|
|
let lineNumber = 1;
|
|
let charCount = 0;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
charCount += lines[i].length + 1; // +1 for newline
|
|
if (charCount >= error.message.match(/position (\d+)/)?.[1]) {
|
|
lineNumber = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
validationDiv.innerHTML = `
|
|
<div class="json-error-message">${errorMessage}</div>
|
|
<div class="json-error-details">
|
|
<strong>Error:</strong> ${errorDetails}<br>
|
|
<strong>Line:</strong> ${lineNumber}<br>
|
|
<strong>Details:</strong> ${error.message}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function saveRawJson(configType) {
|
|
const textareaId = configType === 'main' ? 'main-config-json' : 'secrets-config-json';
|
|
const validationId = configType === 'main' ? 'main-config-validation' : 'secrets-config-validation';
|
|
const textarea = document.getElementById(textareaId);
|
|
const validationDiv = document.getElementById(validationId);
|
|
const jsonText = textarea.value;
|
|
|
|
// First validate the JSON
|
|
try {
|
|
JSON.parse(jsonText);
|
|
} catch (error) {
|
|
showMessage(`Invalid JSON format: ${error.message}`, 'error');
|
|
validateJson(textareaId, validationId);
|
|
return;
|
|
}
|
|
|
|
// Show confirmation dialog
|
|
const configName = configType === 'main' ? 'Main Configuration' : 'Secrets Configuration';
|
|
if (!confirm(`Are you sure you want to save changes to the ${configName}? This will overwrite the current configuration file.`)) {
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
const saveButton = event.target;
|
|
const originalText = saveButton.textContent;
|
|
saveButton.textContent = 'Saving...';
|
|
saveButton.disabled = true;
|
|
|
|
// Send to server
|
|
fetch("{{ url_for('save_raw_json_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
config_type: configType,
|
|
config_data: jsonText
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
// Update validation to show success
|
|
validationDiv.className = 'json-validation success';
|
|
validationDiv.innerHTML = `
|
|
<div class="json-error-message">✅ Configuration saved successfully!</div>
|
|
<div class="json-error-details">
|
|
✓ JSON is valid<br>
|
|
✓ File saved successfully<br>
|
|
✓ Configuration updated
|
|
</div>
|
|
`;
|
|
validationDiv.style.display = 'block';
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
// Show error in validation area
|
|
validationDiv.className = 'json-validation error';
|
|
validationDiv.innerHTML = `
|
|
<div class="json-error-message">❌ Save failed</div>
|
|
<div class="json-error-details">
|
|
<strong>Error:</strong> ${data.message}
|
|
</div>
|
|
`;
|
|
validationDiv.style.display = 'block';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage(`Error saving configuration: ${error}`, 'error');
|
|
// Show error in validation area
|
|
validationDiv.className = 'json-validation error';
|
|
validationDiv.innerHTML = `
|
|
<div class="json-error-message">❌ Save failed</div>
|
|
<div class="json-error-details">
|
|
<strong>Error:</strong> ${error}
|
|
</div>
|
|
`;
|
|
validationDiv.style.display = 'block';
|
|
})
|
|
.finally(() => {
|
|
// Restore button state
|
|
saveButton.textContent = originalText;
|
|
saveButton.disabled = false;
|
|
});
|
|
}
|
|
|
|
function saveSportsConfig() {
|
|
// Collect all sports configuration and save
|
|
const config = {
|
|
mlb: {
|
|
enabled: document.getElementById('mlb_enabled').checked,
|
|
favorite_teams: document.getElementById('mlb_favorites').value.split(',').map(s => s.trim()).filter(s => s),
|
|
live_priority: document.getElementById('mlb_live_priority').checked,
|
|
live_game_duration: parseInt(document.getElementById('mlb_live_game_duration').value),
|
|
show_odds: document.getElementById('mlb_show_odds').checked,
|
|
show_favorite_teams_only: document.getElementById('mlb_show_favorite_teams_only').checked
|
|
},
|
|
nfl_scoreboard: {
|
|
enabled: document.getElementById('nfl_enabled').checked,
|
|
favorite_teams: document.getElementById('nfl_favorites').value.split(',').map(s => s.trim()).filter(s => s),
|
|
live_priority: document.getElementById('nfl_live_priority').checked,
|
|
live_game_duration: parseInt(document.getElementById('nfl_live_game_duration').value),
|
|
show_odds: document.getElementById('nfl_show_odds').checked,
|
|
show_favorite_teams_only: document.getElementById('nfl_show_favorite_teams_only').checked
|
|
},
|
|
nba_scoreboard: {
|
|
enabled: document.getElementById('nba_enabled').checked,
|
|
favorite_teams: document.getElementById('nba_favorites').value.split(',').map(s => s.trim()).filter(s => s),
|
|
live_priority: document.getElementById('nba_live_priority').checked,
|
|
live_game_duration: parseInt(document.getElementById('nba_live_game_duration').value),
|
|
show_odds: document.getElementById('nba_show_odds').checked,
|
|
recent_games_to_show: parseInt(document.getElementById('nba_recent_games_to_show').value),
|
|
upcoming_games_to_show: parseInt(document.getElementById('nba_upcoming_games_to_show').value),
|
|
show_favorite_teams_only: document.getElementById('nba_show_favorite_teams_only').checked
|
|
},
|
|
nhl_scoreboard: {
|
|
enabled: document.getElementById('nhl_enabled').checked,
|
|
favorite_teams: document.getElementById('nhl_favorites').value.split(',').map(s => s.trim()).filter(s => s),
|
|
live_priority: document.getElementById('nhl_live_priority').checked,
|
|
live_game_duration: parseInt(document.getElementById('nhl_live_game_duration').value),
|
|
show_odds: document.getElementById('nhl_show_odds').checked,
|
|
recent_games_to_show: parseInt(document.getElementById('nhl_recent_games_to_show').value),
|
|
upcoming_games_to_show: parseInt(document.getElementById('nhl_upcoming_games_to_show').value),
|
|
show_favorite_teams_only: document.getElementById('nhl_show_favorite_teams_only').checked
|
|
},
|
|
ncaa_fb_scoreboard: {
|
|
enabled: document.getElementById('ncaa_fb_enabled').checked,
|
|
favorite_teams: document.getElementById('ncaa_fb_favorites').value.split(',').map(s => s.trim()).filter(s => s),
|
|
live_priority: document.getElementById('ncaa_fb_live_priority').checked,
|
|
live_game_duration: parseInt(document.getElementById('ncaa_fb_live_game_duration').value),
|
|
show_odds: document.getElementById('ncaa_fb_show_odds').checked,
|
|
show_favorite_teams_only: document.getElementById('ncaa_fb_show_favorite_teams_only').checked,
|
|
show_records: document.getElementById('ncaa_fb_show_records').checked,
|
|
show_ranking: document.getElementById('ncaa_fb_show_ranking').checked
|
|
},
|
|
ncaa_baseball_scoreboard: {
|
|
enabled: document.getElementById('ncaa_baseball_enabled').checked,
|
|
favorite_teams: document.getElementById('ncaa_baseball_favorites').value.split(',').map(s => s.trim()).filter(s => s),
|
|
live_priority: document.getElementById('ncaa_baseball_live_priority').checked,
|
|
live_game_duration: parseInt(document.getElementById('ncaa_baseball_live_game_duration').value),
|
|
show_odds: document.getElementById('ncaa_baseball_show_odds').checked,
|
|
recent_games_to_show: parseInt(document.getElementById('ncaa_baseball_recent_games_to_show').value),
|
|
upcoming_games_to_show: parseInt(document.getElementById('ncaa_baseball_upcoming_games_to_show').value),
|
|
show_favorite_teams_only: document.getElementById('ncaa_baseball_show_favorite_teams_only').checked
|
|
},
|
|
ncaam_basketball_scoreboard: {
|
|
enabled: document.getElementById('ncaam_basketball_enabled').checked,
|
|
favorite_teams: document.getElementById('ncaam_basketball_favorites').value.split(',').map(s => s.trim()).filter(s => s),
|
|
live_priority: document.getElementById('ncaam_basketball_live_priority').checked,
|
|
live_game_duration: parseInt(document.getElementById('ncaam_basketball_live_game_duration').value),
|
|
show_odds: document.getElementById('ncaam_basketball_show_odds').checked,
|
|
recent_games_to_show: parseInt(document.getElementById('ncaam_basketball_recent_games_to_show').value),
|
|
upcoming_games_to_show: parseInt(document.getElementById('ncaam_basketball_upcoming_games_to_show').value),
|
|
show_favorite_teams_only: document.getElementById('ncaam_basketball_show_favorite_teams_only').checked
|
|
},
|
|
milb: {
|
|
enabled: document.getElementById('milb_enabled').checked,
|
|
favorite_teams: document.getElementById('milb_favorites').value.split(',').map(s => s.trim()).filter(s => s),
|
|
live_priority: document.getElementById('milb_live_priority').checked,
|
|
live_game_duration: parseInt(document.getElementById('milb_live_game_duration').value)
|
|
},
|
|
soccer_scoreboard: {
|
|
enabled: document.getElementById('soccer_enabled').checked,
|
|
favorite_teams: document.getElementById('soccer_favorites').value.split(',').map(s => s.trim()).filter(s => s),
|
|
leagues: document.getElementById('soccer_leagues').value.split(',').map(s => s.trim()).filter(s => s),
|
|
live_priority: document.getElementById('soccer_live_priority').checked,
|
|
live_game_duration: parseInt(document.getElementById('soccer_live_game_duration').value),
|
|
show_odds: document.getElementById('soccer_show_odds').checked,
|
|
recent_games_to_show: parseInt(document.getElementById('soccer_recent_games_to_show').value),
|
|
upcoming_games_to_show: parseInt(document.getElementById('soccer_upcoming_games_to_show').value),
|
|
show_favorite_teams_only: document.getElementById('soccer_show_favorite_teams_only').checked
|
|
}
|
|
};
|
|
|
|
// Send to server
|
|
fetch("{{ url_for('save_config_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'config_type': 'main',
|
|
'config_data': JSON.stringify(config)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving sports configuration: ' + error, 'error');
|
|
});
|
|
}
|
|
|
|
function showMessage(message, type) {
|
|
// Create a temporary message element
|
|
const messageDiv = document.createElement('div');
|
|
messageDiv.className = `flash-message ${type}`;
|
|
messageDiv.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
padding: 15px 20px;
|
|
border-radius: 5px;
|
|
color: white;
|
|
font-weight: bold;
|
|
z-index: 1000;
|
|
max-width: 400px;
|
|
word-wrap: break-word;
|
|
`;
|
|
|
|
if (type === 'success') {
|
|
messageDiv.style.backgroundColor = '#4CAF50';
|
|
} else {
|
|
messageDiv.style.backgroundColor = '#f44336';
|
|
}
|
|
|
|
messageDiv.textContent = message;
|
|
document.body.appendChild(messageDiv);
|
|
|
|
// Remove the message after 3 seconds
|
|
setTimeout(() => {
|
|
if (messageDiv.parentNode) {
|
|
messageDiv.parentNode.removeChild(messageDiv);
|
|
}
|
|
}, 3000);
|
|
}
|
|
|
|
// Add form submission handlers for better UX
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Handle schedule form submission
|
|
const scheduleForm = document.getElementById('schedule-form');
|
|
if (scheduleForm) {
|
|
scheduleForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(scheduleForm);
|
|
|
|
fetch("{{ url_for('save_schedule_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'schedule_enabled': formData.get('schedule_enabled') ? 'on' : '',
|
|
'start_time': formData.get('start_time'),
|
|
'end_time': formData.get('end_time')
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving schedule: ' + error, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle display form submission
|
|
const displayForm = document.getElementById('display-form');
|
|
if (displayForm) {
|
|
displayForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(displayForm);
|
|
const config = {
|
|
display: {
|
|
hardware: {
|
|
rows: parseInt(formData.get('rows')),
|
|
cols: parseInt(formData.get('cols')),
|
|
chain_length: parseInt(formData.get('chain_length')),
|
|
parallel: parseInt(formData.get('parallel')),
|
|
brightness: parseInt(formData.get('brightness')),
|
|
hardware_mapping: formData.get('hardware_mapping'),
|
|
scan_mode: parseInt(formData.get('scan_mode')),
|
|
pwm_bits: parseInt(formData.get('pwm_bits')),
|
|
pwm_dither_bits: parseInt(formData.get('pwm_dither_bits')),
|
|
pwm_lsb_nanoseconds: parseInt(formData.get('pwm_lsb_nanoseconds')),
|
|
disable_hardware_pulsing: formData.get('disable_hardware_pulsing') === 'on',
|
|
inverse_colors: formData.get('inverse_colors') === 'on',
|
|
show_refresh_rate: formData.get('show_refresh_rate') === 'on',
|
|
limit_refresh_rate_hz: parseInt(formData.get('limit_refresh_rate_hz'))
|
|
},
|
|
runtime: {
|
|
gpio_slowdown: parseInt(formData.get('gpio_slowdown'))
|
|
},
|
|
use_short_date_format: formData.get('use_short_date_format') === 'on'
|
|
}
|
|
};
|
|
|
|
fetch("{{ url_for('save_config_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'config_type': 'main',
|
|
'config_data': JSON.stringify(config)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving display settings: ' + error, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle weather form submission
|
|
const weatherForm = document.getElementById('weather-form');
|
|
if (weatherForm) {
|
|
weatherForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(weatherForm);
|
|
const config = {
|
|
weather: {
|
|
enabled: formData.get('weather_enabled') === 'on',
|
|
units: formData.get('weather_units'),
|
|
update_interval: parseInt(formData.get('weather_update_interval')),
|
|
display_format: formData.get('weather_display_format')
|
|
},
|
|
location: {
|
|
city: formData.get('weather_city'),
|
|
state: formData.get('weather_state')
|
|
}
|
|
};
|
|
|
|
fetch("{{ url_for('save_config_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'config_type': 'main',
|
|
'config_data': JSON.stringify(config)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving weather settings: ' + error, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle stocks form submission
|
|
const stocksForm = document.getElementById('stocks-form');
|
|
if (stocksForm) {
|
|
stocksForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(stocksForm);
|
|
const symbols = formData.get('stocks_symbols').split(',').map(s => s.trim()).filter(s => s);
|
|
const config = {
|
|
stocks: {
|
|
enabled: formData.get('stocks_enabled') === 'on',
|
|
symbols: symbols,
|
|
update_interval: parseInt(formData.get('stocks_update_interval')),
|
|
scroll_speed: parseInt(formData.get('stocks_scroll_speed')),
|
|
scroll_delay: parseFloat(formData.get('stocks_scroll_delay')),
|
|
toggle_chart: formData.get('stocks_toggle_chart') === 'on',
|
|
display_format: formData.get('stocks_display_format')
|
|
}
|
|
};
|
|
|
|
fetch("{{ url_for('save_config_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'config_type': 'main',
|
|
'config_data': JSON.stringify(config)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving stocks settings: ' + error, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle crypto form submission
|
|
const cryptoForm = document.getElementById('crypto-form');
|
|
if (cryptoForm) {
|
|
cryptoForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(cryptoForm);
|
|
const symbols = formData.get('crypto_symbols').split(',').map(s => s.trim()).filter(s => s);
|
|
const config = {
|
|
crypto: {
|
|
enabled: formData.get('crypto_enabled') === 'on',
|
|
symbols: symbols,
|
|
update_interval: parseInt(formData.get('crypto_update_interval')),
|
|
display_format: formData.get('crypto_display_format'),
|
|
toggle_chart: formData.get('crypto_toggle_chart') === 'on'
|
|
}
|
|
};
|
|
|
|
fetch("{{ url_for('save_config_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'config_type': 'main',
|
|
'config_data': JSON.stringify(config)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving crypto settings: ' + error, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle music form submission
|
|
const musicForm = document.getElementById('music-form');
|
|
if (musicForm) {
|
|
musicForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(musicForm);
|
|
const config = {
|
|
music: {
|
|
enabled: formData.get('music_enabled') === 'on',
|
|
preferred_source: formData.get('music_preferred_source'),
|
|
YTM_COMPANION_URL: formData.get('ytm_companion_url'),
|
|
POLLING_INTERVAL_SECONDS: parseInt(formData.get('music_polling_interval'))
|
|
}
|
|
};
|
|
|
|
fetch("{{ url_for('save_config_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'config_type': 'main',
|
|
'config_data': JSON.stringify(config)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving music settings: ' + error, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle calendar form submission
|
|
const calendarForm = document.getElementById('calendar-form');
|
|
if (calendarForm) {
|
|
calendarForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(calendarForm);
|
|
const calendars = formData.get('calendar_calendars').split(',').map(c => c.trim()).filter(c => c);
|
|
const config = {
|
|
calendar: {
|
|
enabled: formData.get('calendar_enabled') === 'on',
|
|
max_events: parseInt(formData.get('calendar_max_events')),
|
|
update_interval: parseInt(formData.get('calendar_update_interval')),
|
|
calendars: calendars,
|
|
credentials_file: formData.get('calendar_credentials_file'),
|
|
token_file: formData.get('calendar_token_file')
|
|
}
|
|
};
|
|
|
|
fetch("{{ url_for('save_config_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'config_type': 'main',
|
|
'config_data': JSON.stringify(config)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving calendar settings: ' + error, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle secrets form submission
|
|
const secretsForm = document.getElementById('secrets-form-content');
|
|
if (secretsForm) {
|
|
secretsForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(secretsForm);
|
|
const config = {
|
|
weather: {
|
|
api_key: formData.get('weather_api_key')
|
|
},
|
|
youtube: {
|
|
api_key: formData.get('youtube_api_key'),
|
|
channel_id: formData.get('youtube_channel_id')
|
|
},
|
|
music: {
|
|
SPOTIFY_CLIENT_ID: formData.get('spotify_client_id'),
|
|
SPOTIFY_CLIENT_SECRET: formData.get('spotify_client_secret'),
|
|
SPOTIFY_REDIRECT_URI: formData.get('spotify_redirect_uri')
|
|
}
|
|
};
|
|
|
|
fetch("{{ url_for('save_config_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'config_type': 'secrets',
|
|
'config_data': JSON.stringify(config)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving API keys: ' + error, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle durations form submission
|
|
const durationsForm = document.getElementById('durations-form');
|
|
if (durationsForm) {
|
|
durationsForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(durationsForm);
|
|
const config = {
|
|
display: {
|
|
display_durations: {
|
|
clock: parseInt(formData.get('clock_duration')),
|
|
weather: parseInt(formData.get('weather_duration')),
|
|
stocks: parseInt(formData.get('stocks_duration')),
|
|
music: parseInt(formData.get('music_duration')),
|
|
calendar: parseInt(formData.get('calendar_duration')),
|
|
youtube: parseInt(formData.get('youtube_duration')),
|
|
text_display: parseInt(formData.get('text_display_duration')),
|
|
of_the_day: parseInt(formData.get('of_the_day_duration')),
|
|
hourly_forecast: parseInt(formData.get('hourly_forecast_duration')),
|
|
daily_forecast: parseInt(formData.get('daily_forecast_duration')),
|
|
stock_news: parseInt(formData.get('stock_news_duration')),
|
|
odds_ticker: parseInt(formData.get('odds_ticker_duration')),
|
|
nhl_live: parseInt(formData.get('nhl_live_duration')),
|
|
nhl_recent: parseInt(formData.get('nhl_recent_duration')),
|
|
nhl_upcoming: parseInt(formData.get('nhl_upcoming_duration')),
|
|
nba_live: parseInt(formData.get('nba_live_duration')),
|
|
nba_recent: parseInt(formData.get('nba_recent_duration')),
|
|
nba_upcoming: parseInt(formData.get('nba_upcoming_duration')),
|
|
nfl_live: parseInt(formData.get('nfl_live_duration')),
|
|
nfl_recent: parseInt(formData.get('nfl_recent_duration')),
|
|
nfl_upcoming: parseInt(formData.get('nfl_upcoming_duration')),
|
|
ncaa_fb_live: parseInt(formData.get('ncaa_fb_live_duration')),
|
|
ncaa_fb_recent: parseInt(formData.get('ncaa_fb_recent_duration')),
|
|
ncaa_fb_upcoming: parseInt(formData.get('ncaa_fb_upcoming_duration')),
|
|
ncaa_baseball_live: parseInt(formData.get('ncaa_baseball_live_duration')),
|
|
ncaa_baseball_recent: parseInt(formData.get('ncaa_baseball_recent_duration')),
|
|
ncaa_baseball_upcoming: parseInt(formData.get('ncaa_baseball_upcoming_duration')),
|
|
mlb_live: parseInt(formData.get('mlb_live_duration')),
|
|
mlb_recent: parseInt(formData.get('mlb_recent_duration')),
|
|
mlb_upcoming: parseInt(formData.get('mlb_upcoming_duration')),
|
|
milb_live: parseInt(formData.get('milb_live_duration')),
|
|
milb_recent: parseInt(formData.get('milb_recent_duration')),
|
|
milb_upcoming: parseInt(formData.get('milb_upcoming_duration')),
|
|
soccer_live: parseInt(formData.get('soccer_live_duration')),
|
|
soccer_recent: parseInt(formData.get('soccer_recent_duration')),
|
|
soccer_upcoming: parseInt(formData.get('soccer_upcoming_duration')),
|
|
ncaam_basketball_live: parseInt(formData.get('ncaam_basketball_live_duration')),
|
|
ncaam_basketball_recent: parseInt(formData.get('ncaam_basketball_recent_duration')),
|
|
ncaam_basketball_upcoming: parseInt(formData.get('ncaam_basketball_upcoming_duration'))
|
|
}
|
|
}
|
|
};
|
|
|
|
fetch("{{ url_for('save_config_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'config_type': 'main',
|
|
'config_data': JSON.stringify(config)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving display durations: ' + error, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle general form submission
|
|
const generalForm = document.getElementById('general-form');
|
|
if (generalForm) {
|
|
generalForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(generalForm);
|
|
const config = {
|
|
web_display_autostart: formData.get('web_display_autostart') === 'on',
|
|
timezone: formData.get('timezone'),
|
|
location: {
|
|
country: formData.get('location_country')
|
|
}
|
|
};
|
|
|
|
fetch("{{ url_for('save_config_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'config_type': 'main',
|
|
'config_data': JSON.stringify(config)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving general settings: ' + error, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle clock form submission
|
|
const clockForm = document.getElementById('clock-form');
|
|
if (clockForm) {
|
|
clockForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(clockForm);
|
|
const config = {
|
|
clock: {
|
|
enabled: formData.get('clock_enabled') === 'on',
|
|
format: formData.get('clock_format'),
|
|
date_format: formData.get('clock_date_format'),
|
|
update_interval: parseInt(formData.get('clock_update_interval'))
|
|
}
|
|
};
|
|
|
|
fetch("{{ url_for('save_config_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'config_type': 'main',
|
|
'config_data': JSON.stringify(config)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving clock settings: ' + error, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle stock news form submission
|
|
const stockNewsForm = document.getElementById('stock-news-form');
|
|
if (stockNewsForm) {
|
|
stockNewsForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(stockNewsForm);
|
|
const config = {
|
|
stock_news: {
|
|
enabled: formData.get('stock_news_enabled') === 'on',
|
|
update_interval: parseInt(formData.get('stock_news_update_interval')),
|
|
scroll_speed: parseInt(formData.get('stock_news_scroll_speed')),
|
|
scroll_delay: parseFloat(formData.get('stock_news_scroll_delay')),
|
|
max_headlines_per_symbol: parseInt(formData.get('stock_news_max_headlines_per_symbol')),
|
|
headlines_per_rotation: parseInt(formData.get('stock_news_headlines_per_rotation'))
|
|
}
|
|
};
|
|
|
|
fetch("{{ url_for('save_config_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'config_type': 'main',
|
|
'config_data': JSON.stringify(config)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving stock news settings: ' + error, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle odds ticker form submission
|
|
const oddsTickerForm = document.getElementById('odds-ticker-form');
|
|
if (oddsTickerForm) {
|
|
oddsTickerForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(oddsTickerForm);
|
|
const config = {
|
|
odds_ticker: {
|
|
enabled: formData.get('odds_ticker_enabled') === 'on',
|
|
update_interval: parseInt(formData.get('odds_ticker_update_interval')),
|
|
show_favorite_teams_only: formData.get('odds_ticker_show_favorite_teams_only') === 'on',
|
|
games_per_favorite_team: parseInt(formData.get('odds_ticker_games_per_favorite_team')),
|
|
max_games_per_league: parseInt(formData.get('odds_ticker_max_games_per_league')),
|
|
show_odds_only: formData.get('odds_ticker_show_odds_only') === 'on',
|
|
sort_order: formData.get('odds_ticker_sort_order'),
|
|
enabled_leagues: formData.get('odds_ticker_enabled_leagues').split(',').map(s => s.trim()).filter(s => s),
|
|
scroll_speed: parseInt(formData.get('odds_ticker_scroll_speed')),
|
|
scroll_delay: parseFloat(formData.get('odds_ticker_scroll_delay')),
|
|
loop: formData.get('odds_ticker_loop') === 'on',
|
|
future_fetch_days: parseInt(formData.get('odds_ticker_future_fetch_days')),
|
|
show_channel_logos: formData.get('odds_ticker_show_channel_logos') === 'on'
|
|
}
|
|
};
|
|
|
|
fetch("{{ url_for('save_config_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'config_type': 'main',
|
|
'config_data': JSON.stringify(config)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving odds ticker settings: ' + error, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle YouTube form submission
|
|
const youtubeForm = document.getElementById('youtube-form');
|
|
if (youtubeForm) {
|
|
youtubeForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(youtubeForm);
|
|
const config = {
|
|
youtube: {
|
|
enabled: formData.get('youtube_enabled') === 'on',
|
|
channel_id: formData.get('youtube_channel_id'),
|
|
update_interval: parseInt(formData.get('youtube_update_interval'))
|
|
}
|
|
};
|
|
|
|
fetch("{{ url_for('save_config_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'config_type': 'main',
|
|
'config_data': JSON.stringify(config)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving YouTube settings: ' + error, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle text display form submission
|
|
const textDisplayForm = document.getElementById('text-display-form');
|
|
if (textDisplayForm) {
|
|
textDisplayForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(textDisplayForm);
|
|
const config = {
|
|
text_display: {
|
|
enabled: formData.get('text_display_enabled') === 'on',
|
|
text: formData.get('text_display_text'),
|
|
font_path: formData.get('text_display_font_path'),
|
|
font_size: parseInt(formData.get('text_display_font_size')),
|
|
scroll: formData.get('text_display_scroll') === 'on',
|
|
scroll_speed: parseInt(formData.get('text_display_scroll_speed')),
|
|
text_color: formData.get('text_display_text_color').split(',').map(s => parseInt(s.trim())),
|
|
background_color: formData.get('text_display_background_color').split(',').map(s => parseInt(s.trim())),
|
|
scroll_gap_width: parseInt(formData.get('text_display_scroll_gap_width'))
|
|
},
|
|
display: {
|
|
display_durations: {
|
|
text_display: parseInt(formData.get('text_display_duration'))
|
|
}
|
|
}
|
|
};
|
|
|
|
fetch("{{ url_for('save_config_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'config_type': 'main',
|
|
'config_data': JSON.stringify(config)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving text display settings: ' + error, 'error');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle of the day form submission
|
|
const ofTheDayForm = document.getElementById('of_the_day-form');
|
|
if (ofTheDayForm) {
|
|
ofTheDayForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(ofTheDayForm);
|
|
const config = {
|
|
of_the_day: {
|
|
enabled: formData.get('of_the_day_enabled') === 'on',
|
|
update_interval: parseInt(formData.get('of_the_day_update_interval')),
|
|
display_rotate_interval: parseInt(formData.get('of_the_day_display_rotate_interval')),
|
|
subtitle_rotate_interval: parseInt(formData.get('of_the_day_subtitle_rotate_interval')),
|
|
category_order: formData.get('of_the_day_category_order').split(',').map(s => s.trim()).filter(s => s)
|
|
}
|
|
};
|
|
|
|
fetch("{{ url_for('save_config_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
'config_type': 'main',
|
|
'config_data': JSON.stringify(config)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
} else {
|
|
showMessage(data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving of the day settings: ' + error, 'error');
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
function runAction(actionName) {
|
|
const outputElement = document.getElementById('action_output');
|
|
outputElement.textContent = `Running ${actionName.replace(/_/g, ' ')}...`;
|
|
|
|
fetch("{{ url_for('run_action_route') }}", {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: actionName })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
let outputText = `Status: ${data.status}\nMessage: ${data.message}\n`;
|
|
if (data.stdout) outputText += `\n--- STDOUT ---\n${data.stdout}`;
|
|
if (data.stderr) outputText += `\n--- STDERR ---\n${data.stderr}`;
|
|
outputElement.textContent = outputText;
|
|
})
|
|
.catch(error => {
|
|
outputElement.textContent = `Error: ${error}`;
|
|
});
|
|
}
|
|
|
|
// Set default active tab
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
document.querySelector('.tab-link').click();
|
|
|
|
// Add real-time JSON validation for the raw JSON textareas
|
|
const mainConfigTextarea = document.getElementById('main-config-json');
|
|
const secretsConfigTextarea = document.getElementById('secrets-config-json');
|
|
|
|
function addJsonValidationListener(textarea, validationId) {
|
|
let validationTimeout;
|
|
const statusId = validationId.replace('-validation', '-status');
|
|
const statusElement = document.getElementById(statusId);
|
|
|
|
textarea.addEventListener('input', function() {
|
|
// Clear previous timeout
|
|
clearTimeout(validationTimeout);
|
|
|
|
// Set a new timeout to validate after user stops typing
|
|
validationTimeout = setTimeout(() => {
|
|
const jsonText = textarea.value;
|
|
|
|
try {
|
|
JSON.parse(jsonText);
|
|
textarea.classList.remove('error');
|
|
textarea.classList.add('valid');
|
|
if (statusElement) {
|
|
statusElement.textContent = 'VALID';
|
|
statusElement.className = 'json-status valid';
|
|
}
|
|
} catch (error) {
|
|
textarea.classList.remove('valid');
|
|
textarea.classList.add('error');
|
|
if (statusElement) {
|
|
statusElement.textContent = 'INVALID';
|
|
statusElement.className = 'json-status error';
|
|
}
|
|
}
|
|
}, 500); // Wait 500ms after user stops typing
|
|
});
|
|
}
|
|
|
|
if (mainConfigTextarea) {
|
|
addJsonValidationListener(mainConfigTextarea, 'main-config-validation');
|
|
}
|
|
|
|
if (secretsConfigTextarea) {
|
|
addJsonValidationListener(secretsConfigTextarea, 'secrets-config-validation');
|
|
}
|
|
});
|
|
|
|
document.getElementById('refresh-logs-btn').addEventListener('click', fetchLogs);
|
|
|
|
function fetchLogs() {
|
|
const logContent = document.getElementById('log-content');
|
|
logContent.textContent = 'Loading logs...';
|
|
|
|
fetch('/get_logs')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
logContent.textContent = data.logs;
|
|
} else {
|
|
logContent.textContent = `Error loading logs: ${data.message}`;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
logContent.textContent = `Error loading logs: ${error}`;
|
|
});
|
|
}
|
|
|
|
// News Manager Functions
|
|
let newsManagerData = {};
|
|
|
|
function loadNewsManagerData() {
|
|
fetch('/news_manager/status')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
newsManagerData = data.data;
|
|
updateNewsManagerUI();
|
|
} else {
|
|
console.error('Error loading news manager data:', data.message);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading news manager data:', error);
|
|
});
|
|
}
|
|
|
|
function updateNewsManagerUI() {
|
|
// Update enabled toggle
|
|
document.getElementById('news_enabled').checked = newsManagerData.enabled || false;
|
|
|
|
// Update headlines per feed
|
|
document.getElementById('headlines_per_feed').value = newsManagerData.headlines_per_feed || 2;
|
|
|
|
// Update rotation enabled
|
|
document.getElementById('rotation_enabled').checked = newsManagerData.rotation_enabled !== false;
|
|
|
|
// Populate available feeds
|
|
const feedsGrid = document.getElementById('news_feeds_grid');
|
|
feedsGrid.innerHTML = '';
|
|
|
|
if (newsManagerData.available_feeds) {
|
|
newsManagerData.available_feeds.forEach(feed => {
|
|
const isEnabled = newsManagerData.enabled_feeds.includes(feed);
|
|
const feedDiv = document.createElement('div');
|
|
feedDiv.className = 'checkbox-item';
|
|
feedDiv.innerHTML = `
|
|
<label>
|
|
<input type="checkbox" name="news_feed" value="${feed}" ${isEnabled ? 'checked' : ''}>
|
|
${feed}
|
|
</label>
|
|
`;
|
|
feedsGrid.appendChild(feedDiv);
|
|
});
|
|
}
|
|
|
|
// Populate custom feeds
|
|
updateCustomFeedsList();
|
|
|
|
// Update status
|
|
updateNewsStatus();
|
|
}
|
|
|
|
function updateCustomFeedsList() {
|
|
const customFeedsList = document.getElementById('custom_feeds_list');
|
|
customFeedsList.innerHTML = '';
|
|
|
|
if (newsManagerData.custom_feeds) {
|
|
Object.entries(newsManagerData.custom_feeds).forEach(([name, url]) => {
|
|
const feedDiv = document.createElement('div');
|
|
feedDiv.className = 'custom-feed-item';
|
|
feedDiv.innerHTML = `
|
|
<div class="custom-feed-info">
|
|
<strong>${name}</strong>: ${url}
|
|
<button type="button" onclick="removeCustomFeed('${name}')" class="remove-btn">Remove</button>
|
|
</div>
|
|
`;
|
|
customFeedsList.appendChild(feedDiv);
|
|
});
|
|
}
|
|
}
|
|
|
|
function updateNewsStatus() {
|
|
const statusDiv = document.getElementById('news_status');
|
|
const enabledFeeds = newsManagerData.enabled_feeds || [];
|
|
const totalFeeds = enabledFeeds.length + Object.keys(newsManagerData.custom_feeds || {}).length;
|
|
|
|
statusDiv.innerHTML = `
|
|
<div class="status-info">
|
|
<h4>Current Status</h4>
|
|
<p><strong>Enabled:</strong> ${newsManagerData.enabled ? 'Yes' : 'No'}</p>
|
|
<p><strong>Active Feeds:</strong> ${enabledFeeds.join(', ') || 'None'}</p>
|
|
<p><strong>Headlines per Feed:</strong> ${newsManagerData.headlines_per_feed || 2}</p>
|
|
<p><strong>Total Custom Feeds:</strong> ${Object.keys(newsManagerData.custom_feeds || {}).length}</p>
|
|
<p><strong>Rotation Enabled:</strong> ${newsManagerData.rotation_enabled !== false ? 'Yes' : 'No'}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function saveNewsSettings() {
|
|
// Get enabled feeds
|
|
const enabledFeeds = Array.from(document.querySelectorAll('input[name="news_feed"]:checked'))
|
|
.map(input => input.value);
|
|
|
|
const headlinesPerFeed = parseInt(document.getElementById('headlines_per_feed').value);
|
|
const enabled = document.getElementById('news_enabled').checked;
|
|
const rotationEnabled = document.getElementById('rotation_enabled').checked;
|
|
|
|
// Save enabled status first
|
|
fetch('/news_manager/toggle', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ enabled: enabled })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
// Then save feed settings
|
|
return fetch('/news_manager/update_feeds', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
enabled_feeds: enabledFeeds,
|
|
headlines_per_feed: headlinesPerFeed
|
|
})
|
|
});
|
|
} else {
|
|
throw new Error(data.message);
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage('News settings saved successfully!', 'success');
|
|
loadNewsManagerData(); // Refresh the data
|
|
} else {
|
|
showMessage('Error saving news settings: ' + data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error saving news settings: ' + error, 'error');
|
|
});
|
|
}
|
|
|
|
function addCustomFeed() {
|
|
const name = document.getElementById('custom_feed_name').value.trim();
|
|
const url = document.getElementById('custom_feed_url').value.trim();
|
|
|
|
if (!name || !url) {
|
|
showMessage('Please enter both feed name and URL', 'error');
|
|
return;
|
|
}
|
|
|
|
fetch('/news_manager/add_custom_feed', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: name, url: url })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
document.getElementById('custom_feed_name').value = '';
|
|
document.getElementById('custom_feed_url').value = '';
|
|
loadNewsManagerData(); // Refresh the data
|
|
} else {
|
|
showMessage('Error adding custom feed: ' + data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error adding custom feed: ' + error, 'error');
|
|
});
|
|
}
|
|
|
|
function removeCustomFeed(name) {
|
|
if (!confirm(`Are you sure you want to remove the feed "${name}"?`)) {
|
|
return;
|
|
}
|
|
|
|
fetch('/news_manager/remove_custom_feed', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: name })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
showMessage(data.message, 'success');
|
|
loadNewsManagerData(); // Refresh the data
|
|
} else {
|
|
showMessage('Error removing custom feed: ' + data.message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showMessage('Error removing custom feed: ' + error, 'error');
|
|
});
|
|
}
|
|
|
|
function refreshNewsStatus() {
|
|
loadNewsManagerData();
|
|
showMessage('News status refreshed', 'success');
|
|
}
|
|
|
|
// Load news manager data when the news tab is opened
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Override the openTab function to load news data when news tab is opened
|
|
const originalOpenTab = window.openTab;
|
|
window.openTab = function(evt, tabName) {
|
|
originalOpenTab(evt, tabName);
|
|
if (tabName === 'news') {
|
|
loadNewsManagerData();
|
|
}
|
|
};
|
|
});
|
|
|
|
</script>
|
|
</body>
|
|
</html> |