6a0cc12ca1
Validate / validate (push) Failing after 14s
- Visual display layout visualization showing all monitors in their relative positions - Displays are scaled proportionally and positioned based on actual coordinates - Primary displays marked with star icon and green borders - Secondary displays with gray borders - Hover effects on display visualization with detailed tooltips - Color-coded legend explaining primary/secondary displays - Enhanced display cards with primary/secondary badges - Added display index to display cards for clarity - Added lightbulb emoji favicon for browser tab This makes it much easier to understand multi-monitor setups and identify which physical monitor corresponds to which display index when configuring WLED devices. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
433 lines
21 KiB
HTML
433 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>WLED Screen Controller</title>
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='0.9em' font-size='90'>💡</text></svg>">
|
|
<link rel="stylesheet" href="/static/style.css">
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1>WLED Screen Controller</h1>
|
|
<div class="server-info">
|
|
<span id="server-version">Version: Loading...</span>
|
|
<span id="server-status" class="status-badge">●</span>
|
|
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme">
|
|
<span id="theme-icon">🌙</span>
|
|
</button>
|
|
<span id="auth-status" style="margin-left: 10px; display: none;">
|
|
<span id="logged-in-user" style="color: #4CAF50; margin-right: 8px;">●</span>
|
|
</span>
|
|
<button id="login-btn" class="btn btn-primary" onclick="showLogin()" style="display: none; padding: 6px 16px; font-size: 0.85rem; margin-left: 10px;">
|
|
🔑 Login
|
|
</button>
|
|
<button id="logout-btn" class="btn btn-danger" onclick="logout()" style="display: none; padding: 6px 16px; font-size: 0.85rem; margin-left: 10px;">
|
|
🚪 Logout
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<section class="displays-section">
|
|
<h2>Available Displays</h2>
|
|
|
|
<!-- Visual Layout Preview -->
|
|
<div class="display-layout-preview">
|
|
<h3>Display Layout</h3>
|
|
<div id="display-layout-canvas" class="display-layout-canvas">
|
|
<div class="loading">Loading layout...</div>
|
|
</div>
|
|
<div class="layout-legend">
|
|
<span class="legend-item"><span class="legend-dot primary"></span> Primary Display</span>
|
|
<span class="legend-item"><span class="legend-dot secondary"></span> Secondary Display</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Display Cards -->
|
|
<h3 style="margin-top: 30px;">Display Information</h3>
|
|
<div id="displays-list" class="displays-grid">
|
|
<div class="loading">Loading displays...</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="devices-section">
|
|
<h2>WLED Devices</h2>
|
|
<div id="devices-list" class="devices-grid">
|
|
<div class="loading">Loading devices...</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="add-device-section">
|
|
<h2>Add New Device</h2>
|
|
<div class="info-banner" style="margin-bottom: 20px; padding: 12px; background: rgba(33, 150, 243, 0.1); border-left: 4px solid #2196F3; border-radius: 4px;">
|
|
<strong>📱 WLED Configuration:</strong> Configure your WLED device (effects, segments, color order, power limits, etc.) using the
|
|
<a href="https://kno.wled.ge/" target="_blank" rel="noopener" style="color: #2196F3; text-decoration: underline;">official WLED app</a>.
|
|
This controller sends pixel color data and controls brightness per device.
|
|
</div>
|
|
<form id="add-device-form">
|
|
<div class="form-group">
|
|
<label for="device-name">Device Name:</label>
|
|
<input type="text" id="device-name" placeholder="Living Room TV" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="device-url">WLED URL:</label>
|
|
<input type="url" id="device-url" placeholder="http://192.168.1.100" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="device-led-count">LED Count:</label>
|
|
<input type="number" id="device-led-count" value="150" min="1" required>
|
|
<small class="input-hint">Number of LEDs configured in your WLED device</small>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Add Device</button>
|
|
</form>
|
|
</section>
|
|
</div>
|
|
|
|
<div id="toast" class="toast"></div>
|
|
|
|
<!-- Calibration Modal -->
|
|
<div id="calibration-modal" class="modal">
|
|
<div class="modal-content" style="max-width: 700px;">
|
|
<div class="modal-header">
|
|
<h2>📐 LED Calibration</h2>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="calibration-device-id">
|
|
<p style="margin-bottom: 20px; color: var(--text-secondary);">
|
|
Configure how your LED strip is mapped to screen edges. Use test buttons to verify each edge lights up correctly.
|
|
</p>
|
|
|
|
<!-- Visual Preview -->
|
|
<div style="margin-bottom: 25px;">
|
|
<div style="position: relative; width: 400px; height: 250px; margin: 0 auto; background: var(--card-bg); border: 2px solid var(--border-color); border-radius: 8px;">
|
|
<!-- Screen representation -->
|
|
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 300px; height: 180px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 4px; display: flex; align-items: center; justify-content: center; color: white; font-size: 14px;">
|
|
Screen
|
|
</div>
|
|
|
|
<!-- Edge labels -->
|
|
<div style="position: absolute; top: 5px; left: 50%; transform: translateX(-50%); font-size: 12px; color: var(--text-secondary);">
|
|
Top: <span id="preview-top-count">0</span> LEDs
|
|
</div>
|
|
<div style="position: absolute; right: 5px; top: 50%; transform: translateY(-50%) rotate(90deg); font-size: 12px; color: var(--text-secondary); white-space: nowrap;">
|
|
Right: <span id="preview-right-count">0</span> LEDs
|
|
</div>
|
|
<div style="position: absolute; bottom: 5px; left: 50%; transform: translateX(-50%); font-size: 12px; color: var(--text-secondary);">
|
|
Bottom: <span id="preview-bottom-count">0</span> LEDs
|
|
</div>
|
|
<div style="position: absolute; left: 5px; top: 50%; transform: translateY(-50%) rotate(-90deg); font-size: 12px; color: var(--text-secondary); white-space: nowrap;">
|
|
Left: <span id="preview-left-count">0</span> LEDs
|
|
</div>
|
|
|
|
<!-- Starting position indicator -->
|
|
<div id="start-indicator" style="position: absolute; bottom: 10px; left: 10px; width: 12px; height: 12px; background: #4CAF50; border-radius: 50%; border: 2px solid white;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Layout Configuration -->
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px;">
|
|
<div class="form-group">
|
|
<label for="cal-start-position">Starting Position:</label>
|
|
<select id="cal-start-position" onchange="updateCalibrationPreview()">
|
|
<option value="bottom_left">Bottom Left</option>
|
|
<option value="bottom_right">Bottom Right</option>
|
|
<option value="top_left">Top Left</option>
|
|
<option value="top_right">Top Right</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="cal-layout">Direction:</label>
|
|
<select id="cal-layout" onchange="updateCalibrationPreview()">
|
|
<option value="clockwise">Clockwise</option>
|
|
<option value="counterclockwise">Counterclockwise</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- LED Counts per Edge -->
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px;">
|
|
<div class="form-group">
|
|
<label for="cal-top-leds">Top LEDs:</label>
|
|
<input type="number" id="cal-top-leds" min="0" value="0" oninput="updateCalibrationPreview()">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="cal-right-leds">Right LEDs:</label>
|
|
<input type="number" id="cal-right-leds" min="0" value="0" oninput="updateCalibrationPreview()">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="cal-bottom-leds">Bottom LEDs:</label>
|
|
<input type="number" id="cal-bottom-leds" min="0" value="0" oninput="updateCalibrationPreview()">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="cal-left-leds">Left LEDs:</label>
|
|
<input type="number" id="cal-left-leds" min="0" value="0" oninput="updateCalibrationPreview()">
|
|
</div>
|
|
</div>
|
|
|
|
<div style="padding: 10px; background: rgba(255, 193, 7, 0.1); border-left: 4px solid #FFC107; border-radius: 4px; margin-bottom: 20px;">
|
|
<strong>Total LEDs:</strong> <span id="cal-total-leds">0</span> / <span id="cal-device-led-count">0</span>
|
|
</div>
|
|
|
|
<!-- Test Buttons -->
|
|
<div style="margin-bottom: 15px;">
|
|
<p style="font-weight: 600; margin-bottom: 10px;">Test Edges (lights up each edge):</p>
|
|
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;">
|
|
<button class="btn btn-secondary" onclick="testCalibrationEdge('top')" style="font-size: 0.9rem; padding: 8px;">
|
|
⬆️ Top
|
|
</button>
|
|
<button class="btn btn-secondary" onclick="testCalibrationEdge('right')" style="font-size: 0.9rem; padding: 8px;">
|
|
➡️ Right
|
|
</button>
|
|
<button class="btn btn-secondary" onclick="testCalibrationEdge('bottom')" style="font-size: 0.9rem; padding: 8px;">
|
|
⬇️ Bottom
|
|
</button>
|
|
<button class="btn btn-secondary" onclick="testCalibrationEdge('left')" style="font-size: 0.9rem; padding: 8px;">
|
|
⬅️ Left
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="calibration-error" class="error-message" style="display: none;"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" onclick="closeCalibrationModal()">Cancel</button>
|
|
<button class="btn btn-primary" onclick="saveCalibration()">Save Calibration</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Device Settings Modal -->
|
|
<div id="device-settings-modal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2>⚙️ Device Settings</h2>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="device-settings-form">
|
|
<input type="hidden" id="settings-device-id">
|
|
|
|
<div class="form-group">
|
|
<label for="settings-device-name">Device Name:</label>
|
|
<input type="text" id="settings-device-name" placeholder="Living Room TV" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="settings-device-url">WLED URL:</label>
|
|
<input type="url" id="settings-device-url" placeholder="http://192.168.1.100" required>
|
|
<small class="input-hint">IP address or hostname of your WLED device</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="settings-device-led-count">LED Count:</label>
|
|
<input type="number" id="settings-device-led-count" min="1" required>
|
|
<small class="input-hint">Number of LEDs configured in your WLED device</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="settings-device-brightness">Brightness: <span id="brightness-value">100%</span></label>
|
|
<input type="range" id="settings-device-brightness" min="0" max="100" value="100"
|
|
oninput="document.getElementById('brightness-value').textContent = this.value + '%'"
|
|
style="width: 100%;">
|
|
<small class="input-hint">Global brightness for this WLED device (0-100%)</small>
|
|
</div>
|
|
|
|
<div id="settings-error" class="error-message" style="display: none;"></div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" onclick="closeDeviceSettingsModal()">Cancel</button>
|
|
<button class="btn btn-primary" onclick="saveDeviceSettings()">Save Changes</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Login Modal -->
|
|
<div id="api-key-modal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2>🔑 Login to WLED Controller</h2>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="modal-description">
|
|
Please enter your API key to authenticate and access the WLED Screen Controller.
|
|
</p>
|
|
<div class="form-group">
|
|
<label for="api-key-input">API Key:</label>
|
|
<div class="password-input-wrapper">
|
|
<input
|
|
type="password"
|
|
id="api-key-input"
|
|
placeholder="Enter your API key..."
|
|
autocomplete="off"
|
|
>
|
|
<button type="button" class="password-toggle" onclick="togglePasswordVisibility()">
|
|
👁️
|
|
</button>
|
|
</div>
|
|
<small class="input-hint">Your API key will be stored securely in your browser's local storage.</small>
|
|
</div>
|
|
<div id="api-key-error" class="error-message" style="display: none;"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" onclick="closeApiKeyModal()" id="modal-cancel-btn">Cancel</button>
|
|
<button class="btn btn-primary" onclick="submitApiKey()">Login</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/static/app.js"></script>
|
|
<script>
|
|
// Initialize theme
|
|
const savedTheme = localStorage.getItem('theme') || 'dark';
|
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
updateThemeIcon(savedTheme);
|
|
|
|
function updateThemeIcon(theme) {
|
|
const icon = document.getElementById('theme-icon');
|
|
icon.textContent = theme === 'dark' ? '☀️' : '🌙';
|
|
}
|
|
|
|
function toggleTheme() {
|
|
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
|
|
document.documentElement.setAttribute('data-theme', newTheme);
|
|
localStorage.setItem('theme', newTheme);
|
|
updateThemeIcon(newTheme);
|
|
showToast(`Switched to ${newTheme} theme`, 'info');
|
|
}
|
|
|
|
// Initialize auth state
|
|
function updateAuthUI() {
|
|
const apiKey = localStorage.getItem('wled_api_key');
|
|
const loginBtn = document.getElementById('login-btn');
|
|
const logoutBtn = document.getElementById('logout-btn');
|
|
const authStatus = document.getElementById('auth-status');
|
|
const loggedInUser = document.getElementById('logged-in-user');
|
|
|
|
if (apiKey) {
|
|
// Logged in
|
|
loginBtn.style.display = 'none';
|
|
logoutBtn.style.display = 'inline-block';
|
|
authStatus.style.display = 'inline';
|
|
|
|
// Show masked key
|
|
const masked = apiKey.substring(0, 8) + '...';
|
|
loggedInUser.textContent = `● Authenticated`;
|
|
loggedInUser.title = `API Key: ${masked}`;
|
|
} else {
|
|
// Logged out
|
|
loginBtn.style.display = 'inline-block';
|
|
logoutBtn.style.display = 'none';
|
|
authStatus.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function showLogin() {
|
|
showApiKeyModal('Enter your API key to login and access the controller.');
|
|
document.getElementById('modal-cancel-btn').style.display = 'inline-block';
|
|
}
|
|
|
|
function logout() {
|
|
if (confirm('Are you sure you want to logout?')) {
|
|
localStorage.removeItem('wled_api_key');
|
|
apiKey = null;
|
|
updateAuthUI();
|
|
showToast('Logged out successfully', 'info');
|
|
|
|
// Clear the UI
|
|
document.getElementById('devices-list').innerHTML = '<div class="loading">Please login to view devices</div>';
|
|
document.getElementById('displays-list').innerHTML = '<div class="loading">Please login to view displays</div>';
|
|
}
|
|
}
|
|
|
|
// Initialize on load
|
|
updateAuthUI();
|
|
|
|
// Modal functions
|
|
function togglePasswordVisibility() {
|
|
const input = document.getElementById('api-key-input');
|
|
const button = document.querySelector('.password-toggle');
|
|
if (input.type === 'password') {
|
|
input.type = 'text';
|
|
button.textContent = '🙈';
|
|
} else {
|
|
input.type = 'password';
|
|
button.textContent = '👁️';
|
|
}
|
|
}
|
|
|
|
function showApiKeyModal(message, hideCancel = false) {
|
|
const modal = document.getElementById('api-key-modal');
|
|
const description = document.querySelector('.modal-description');
|
|
const input = document.getElementById('api-key-input');
|
|
const error = document.getElementById('api-key-error');
|
|
const cancelBtn = document.getElementById('modal-cancel-btn');
|
|
|
|
if (message) {
|
|
description.textContent = message;
|
|
}
|
|
|
|
input.value = '';
|
|
input.placeholder = 'Enter your API key...';
|
|
error.style.display = 'none';
|
|
modal.style.display = 'flex';
|
|
|
|
// Hide cancel button if this is required login (no existing session)
|
|
cancelBtn.style.display = hideCancel ? 'none' : 'inline-block';
|
|
|
|
setTimeout(() => input.focus(), 100);
|
|
}
|
|
|
|
function closeApiKeyModal() {
|
|
const modal = document.getElementById('api-key-modal');
|
|
modal.style.display = 'none';
|
|
}
|
|
|
|
function submitApiKey() {
|
|
const input = document.getElementById('api-key-input');
|
|
const error = document.getElementById('api-key-error');
|
|
const key = input.value.trim();
|
|
|
|
if (!key) {
|
|
error.textContent = 'Please enter an API key';
|
|
error.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
// Store the key
|
|
localStorage.setItem('wled_api_key', key);
|
|
apiKey = key;
|
|
updateAuthUI();
|
|
|
|
closeApiKeyModal();
|
|
showToast('Logged in successfully!', 'success');
|
|
|
|
// Reload data
|
|
loadServerInfo();
|
|
loadDisplays();
|
|
loadDevices();
|
|
|
|
// Start auto-refresh if not already running
|
|
if (!refreshInterval) {
|
|
startAutoRefresh();
|
|
}
|
|
}
|
|
|
|
// Handle Enter key in modal
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
document.getElementById('api-key-input').addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
submitApiKey();
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|