Some checks failed
Validate / validate (push) Failing after 8s
- Add close X button to all modal headers (acts as Cancel) - Replace Cancel/Save labels with icon buttons (✕/✓) - Remove header/footer separator lines, reduce spacing - Fix canvas re-render on resize via ResizeObserver - Move calibration hint to top as section-tip - Increase toggle zones to 16px, make borders more visible - Differentiate min/max ticks (long) from intermediate (short) - Sync toggle zones and ticks with span position - Fix span handle z-index to stay above LED input - Add total LED label click to toggle edge input visibility - Remove corner icon scale on hover - Direction arrows fixed at full-edge midpoint (unaffected by span) - Span bars fill full edge area with 2px border radius Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
448 lines
23 KiB
HTML
448 lines
23 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 style="visibility: hidden;">
|
|
<div class="container">
|
|
<header>
|
|
<div class="header-title">
|
|
<span id="server-status" class="status-badge">●</span>
|
|
<h1 data-i18n="app.title">WLED Screen Controller</h1>
|
|
<span id="server-version"><span id="version-number"></span></span>
|
|
</div>
|
|
<div class="server-info">
|
|
<button class="theme-toggle" onclick="toggleTheme()" data-i18n-title="theme.toggle" title="Toggle theme">
|
|
<span id="theme-icon">🌙</span>
|
|
</button>
|
|
<select id="locale-select" onchange="changeLocale()" data-i18n-title="locale.change" title="Change language" style="margin-left: 10px; padding: 6px 12px; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-color); color: var(--text-color); font-size: 0.9rem; cursor: pointer;">
|
|
<option value="en">English</option>
|
|
<option value="ru">Русский</option>
|
|
</select>
|
|
<button id="login-btn" class="btn btn-primary" onclick="showLogin()" style="display: none; padding: 6px 16px; font-size: 0.85rem; margin-left: 10px;">
|
|
🔑 <span data-i18n="auth.login">Login</span>
|
|
</button>
|
|
<button id="logout-btn" class="btn btn-danger" onclick="logout()" style="display: none; padding: 6px 16px; font-size: 0.85rem; margin-left: 10px;">
|
|
🚪 <span data-i18n="auth.logout">Logout</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="tabs">
|
|
<div class="tab-bar">
|
|
<button class="tab-btn active" data-tab="devices" onclick="switchTab('devices')"><span data-i18n="devices.title">💡 Devices</span></button>
|
|
<button class="tab-btn" data-tab="displays" onclick="switchTab('displays')"><span data-i18n="displays.layout">🖥️ Displays</span></button>
|
|
</div>
|
|
|
|
<div class="tab-panel active" id="tab-devices">
|
|
<p class="section-tip">
|
|
<strong><span data-i18n="devices.wled_config">WLED Configuration:</span></strong> <span data-i18n="devices.wled_note">Configure your WLED device (effects, segments, color order, power limits, etc.) using the</span>
|
|
<a href="https://kno.wled.ge/" target="_blank" rel="noopener" data-i18n="devices.wled_link">official WLED app</a>
|
|
<span data-i18n="devices.wled_note_or">or the built-in</span>
|
|
<a href="#" class="wled-webui-link" data-i18n="devices.wled_webui_link">WLED Web UI</a>
|
|
<span data-i18n="devices.wled_note_webui">(open your device's IP in a browser).</span>
|
|
<span data-i18n="devices.wled_note2">This controller sends pixel color data and controls brightness per device.</span>
|
|
</p>
|
|
<div id="devices-list" class="devices-grid">
|
|
<div class="loading" data-i18n="devices.loading">Loading devices...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-panel" id="tab-displays">
|
|
<div class="display-layout-preview">
|
|
<div id="display-layout-canvas" class="display-layout-canvas">
|
|
<div class="loading" data-i18n="displays.loading">Loading layout...</div>
|
|
</div>
|
|
</div>
|
|
<div id="displays-list" style="display: none;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<footer class="app-footer">
|
|
<div class="footer-content">
|
|
<p>
|
|
Created by <strong>Alexei Dolgolyov</strong>
|
|
• <a href="mailto:dolgolyov.alexei@gmail.com">dolgolyov.alexei@gmail.com</a>
|
|
• <a href="https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed" target="_blank" rel="noopener">Source Code</a>
|
|
</p>
|
|
</div>
|
|
</footer>
|
|
</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 data-i18n="calibration.title">📐 LED Calibration</h2>
|
|
<button class="modal-close-btn" onclick="closeCalibrationModal()" title="Close">✕</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="calibration-device-id">
|
|
<p class="section-tip" data-i18n="calibration.preview.click_hint">Click an edge to toggle test LEDs on/off</p>
|
|
<!-- Interactive Preview with integrated LED inputs and test toggles -->
|
|
<div style="margin-bottom: 12px; padding: 0 24px;">
|
|
<div class="calibration-preview">
|
|
<!-- Screen with direction toggle, total LEDs, and offset -->
|
|
<div class="preview-screen">
|
|
<div class="preview-screen-total" onclick="toggleEdgeInputs()" title="Toggle edge LED inputs"><span id="cal-total-leds-inline">0</span> / <span id="cal-device-led-count-inline">0</span></div>
|
|
<div class="preview-screen-controls">
|
|
<button type="button" class="direction-toggle" onclick="toggleDirection()" title="Toggle direction">
|
|
<span id="direction-icon">↻</span> <span id="direction-label">CW</span>
|
|
</button>
|
|
<label class="offset-control" title="LED offset from LED 0 to start corner">
|
|
<span>⊕</span>
|
|
<input type="number" id="cal-offset" min="0" value="0" oninput="updateCalibrationPreview()">
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edge bars with span controls and LED count inputs -->
|
|
<div class="preview-edge edge-top">
|
|
<div class="edge-span-bar" data-edge="top">
|
|
<div class="edge-span-handle edge-span-handle-start" data-edge="top" data-handle="start"></div>
|
|
<div class="edge-span-handle edge-span-handle-end" data-edge="top" data-handle="end"></div>
|
|
</div>
|
|
<input type="number" id="cal-top-leds" class="edge-led-input" min="0" value="0"
|
|
oninput="updateCalibrationPreview()">
|
|
</div>
|
|
<div class="preview-edge edge-right">
|
|
<div class="edge-span-bar" data-edge="right">
|
|
<div class="edge-span-handle edge-span-handle-start" data-edge="right" data-handle="start"></div>
|
|
<div class="edge-span-handle edge-span-handle-end" data-edge="right" data-handle="end"></div>
|
|
</div>
|
|
<input type="number" id="cal-right-leds" class="edge-led-input" min="0" value="0"
|
|
oninput="updateCalibrationPreview()">
|
|
</div>
|
|
<div class="preview-edge edge-bottom">
|
|
<div class="edge-span-bar" data-edge="bottom">
|
|
<div class="edge-span-handle edge-span-handle-start" data-edge="bottom" data-handle="start"></div>
|
|
<div class="edge-span-handle edge-span-handle-end" data-edge="bottom" data-handle="end"></div>
|
|
</div>
|
|
<input type="number" id="cal-bottom-leds" class="edge-led-input" min="0" value="0"
|
|
oninput="updateCalibrationPreview()">
|
|
</div>
|
|
<div class="preview-edge edge-left">
|
|
<div class="edge-span-bar" data-edge="left">
|
|
<div class="edge-span-handle edge-span-handle-start" data-edge="left" data-handle="start"></div>
|
|
<div class="edge-span-handle edge-span-handle-end" data-edge="left" data-handle="end"></div>
|
|
</div>
|
|
<input type="number" id="cal-left-leds" class="edge-led-input" min="0" value="0"
|
|
oninput="updateCalibrationPreview()">
|
|
</div>
|
|
|
|
<!-- Edge test toggle zones (outside container border, in tick area) -->
|
|
<div class="edge-toggle toggle-top" onclick="toggleTestEdge('top')"></div>
|
|
<div class="edge-toggle toggle-right" onclick="toggleTestEdge('right')"></div>
|
|
<div class="edge-toggle toggle-bottom" onclick="toggleTestEdge('bottom')"></div>
|
|
<div class="edge-toggle toggle-left" onclick="toggleTestEdge('left')"></div>
|
|
|
|
<!-- Corner start position buttons -->
|
|
<div class="preview-corner corner-top-left" onclick="setStartPosition('top_left')">●</div>
|
|
<div class="preview-corner corner-top-right" onclick="setStartPosition('top_right')">●</div>
|
|
<div class="preview-corner corner-bottom-left" onclick="setStartPosition('bottom_left')">●</div>
|
|
<div class="preview-corner corner-bottom-right" onclick="setStartPosition('bottom_right')">●</div>
|
|
|
|
<!-- Canvas overlay for ticks, arrows, start label -->
|
|
<canvas id="calibration-preview-canvas"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hidden selects (used by saveCalibration) -->
|
|
<div style="display: none;">
|
|
<select id="cal-start-position">
|
|
<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>
|
|
<select id="cal-layout">
|
|
<option value="clockwise">Clockwise</option>
|
|
<option value="counterclockwise">Counterclockwise</option>
|
|
</select>
|
|
</div>
|
|
|
|
|
|
<div id="calibration-error" class="error-message" style="display: none;"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-icon btn-secondary" onclick="closeCalibrationModal()" title="Cancel">✕</button>
|
|
<button class="btn btn-icon btn-primary" onclick="saveCalibration()" title="Save">✓</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Device Settings Modal -->
|
|
<div id="device-settings-modal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 data-i18n="settings.title">⚙️ Device Settings</h2>
|
|
<button class="modal-close-btn" onclick="closeDeviceSettingsModal()" title="Close">✕</button>
|
|
</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" data-i18n="device.name">Device Name:</label>
|
|
<input type="text" id="settings-device-name" data-i18n-placeholder="device.name.placeholder" placeholder="Living Room TV" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="settings-device-url" data-i18n="device.url">WLED URL:</label>
|
|
<input type="url" id="settings-device-url" data-i18n-placeholder="device.url.placeholder" placeholder="http://192.168.1.100" required>
|
|
<small class="input-hint" data-i18n="settings.url.hint">IP address or hostname of your WLED device</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="settings-display-index" data-i18n="settings.display_index">Display:</label>
|
|
<select id="settings-display-index"></select>
|
|
<small class="input-hint" data-i18n="settings.display_index.hint">Which screen to capture for this device</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="settings-health-interval" data-i18n="settings.health_interval">Health Check Interval (s):</label>
|
|
<input type="number" id="settings-health-interval" min="5" max="600" value="30">
|
|
<small class="input-hint" data-i18n="settings.health_interval.hint">How often to check the WLED device status (5-600 seconds)</small>
|
|
</div>
|
|
|
|
<div id="settings-error" class="error-message" style="display: none;"></div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-icon btn-secondary" onclick="closeDeviceSettingsModal()" title="Cancel">✕</button>
|
|
<button class="btn btn-icon btn-primary" onclick="saveDeviceSettings()" title="Save">✓</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Login Modal -->
|
|
<div id="api-key-modal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 data-i18n="auth.title">🔑 Login to WLED Controller</h2>
|
|
<button class="modal-close-btn" id="modal-close-x-btn" onclick="closeApiKeyModal()" title="Close">✕</button>
|
|
</div>
|
|
<form id="api-key-form" onsubmit="submitApiKey(event)">
|
|
<div class="modal-body">
|
|
<p class="modal-description" data-i18n="auth.message">
|
|
Please enter your API key to authenticate and access the WLED Screen Controller.
|
|
</p>
|
|
<div class="form-group">
|
|
<label for="api-key-input" data-i18n="auth.label">API Key:</label>
|
|
<div class="password-input-wrapper">
|
|
<input
|
|
type="password"
|
|
id="api-key-input"
|
|
data-i18n-placeholder="auth.placeholder"
|
|
placeholder="Enter your API key..."
|
|
autocomplete="off"
|
|
>
|
|
<button type="button" class="password-toggle" onclick="togglePasswordVisibility()">
|
|
👁️
|
|
</button>
|
|
</div>
|
|
<small class="input-hint" data-i18n="auth.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 type="button" class="btn btn-icon btn-secondary" onclick="closeApiKeyModal()" id="modal-cancel-btn" title="Cancel">✕</button>
|
|
<button type="submit" class="btn btn-icon btn-primary" title="Login">✓</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Confirmation Modal -->
|
|
<div id="confirm-modal" class="modal">
|
|
<div class="modal-content" style="max-width: 450px;">
|
|
<div class="modal-header">
|
|
<h2 id="confirm-title">Confirm Action</h2>
|
|
<button class="modal-close-btn" onclick="closeConfirmModal(false)" title="Close">✕</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p id="confirm-message" class="modal-description"></p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" id="confirm-no-btn" onclick="closeConfirmModal(false)">No</button>
|
|
<button class="btn btn-danger" id="confirm-yes-btn" onclick="closeConfirmModal(true)">Yes</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Device Modal -->
|
|
<div id="add-device-modal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 data-i18n="devices.add">Add New Device</h2>
|
|
<button class="modal-close-btn" onclick="closeAddDeviceModal()" title="Close">✕</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="add-device-form">
|
|
<div class="form-group">
|
|
<label for="device-name" data-i18n="device.name">Device Name:</label>
|
|
<input type="text" id="device-name" data-i18n-placeholder="device.name.placeholder" placeholder="Living Room TV" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="device-url" data-i18n="device.url">WLED URL:</label>
|
|
<input type="url" id="device-url" data-i18n-placeholder="device.url.placeholder" placeholder="http://192.168.1.100" required>
|
|
</div>
|
|
<div id="add-device-error" class="error-message" style="display: none;"></div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-icon btn-secondary" onclick="closeAddDeviceModal()" title="Cancel">✕</button>
|
|
<button class="btn btn-icon btn-primary" onclick="document.getElementById('add-device-form').requestSubmit()" title="Add Device">✓</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');
|
|
|
|
if (apiKey) {
|
|
loginBtn.style.display = 'none';
|
|
logoutBtn.style.display = 'inline-block';
|
|
} else {
|
|
loginBtn.style.display = 'inline-block';
|
|
logoutBtn.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function showLogin() {
|
|
showApiKeyModal(t('auth.message'));
|
|
document.getElementById('modal-cancel-btn').style.display = 'inline-block';
|
|
}
|
|
|
|
async function logout() {
|
|
const confirmed = await showConfirm(t('auth.logout.confirm'));
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
|
|
localStorage.removeItem('wled_api_key');
|
|
apiKey = null;
|
|
updateAuthUI();
|
|
showToast(t('auth.logout.success'), 'info');
|
|
|
|
// Clear the UI
|
|
document.getElementById('devices-list').innerHTML = `<div class="loading">${t('auth.please_login')} devices</div>`;
|
|
document.getElementById('displays-list').innerHTML = `<div class="loading">${t('auth.please_login')} 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';
|
|
lockBody();
|
|
|
|
// Hide cancel button and close X if this is required login (no existing session)
|
|
cancelBtn.style.display = hideCancel ? 'none' : 'inline-block';
|
|
const closeXBtn = document.getElementById('modal-close-x-btn');
|
|
if (closeXBtn) closeXBtn.style.display = hideCancel ? 'none' : '';
|
|
|
|
setTimeout(() => input.focus(), 100);
|
|
}
|
|
|
|
function closeApiKeyModal() {
|
|
const modal = document.getElementById('api-key-modal');
|
|
modal.style.display = 'none';
|
|
unlockBody();
|
|
}
|
|
|
|
function submitApiKey(event) {
|
|
if (event) {
|
|
event.preventDefault();
|
|
}
|
|
|
|
const input = document.getElementById('api-key-input');
|
|
const error = document.getElementById('api-key-error');
|
|
const key = input.value.trim();
|
|
|
|
if (!key) {
|
|
error.textContent = t('auth.error.required');
|
|
error.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
// Store the key
|
|
localStorage.setItem('wled_api_key', key);
|
|
apiKey = key;
|
|
updateAuthUI();
|
|
|
|
closeApiKeyModal();
|
|
showToast(t('auth.success'), 'success');
|
|
|
|
// Reload data
|
|
loadServerInfo();
|
|
loadDisplays();
|
|
loadDevices();
|
|
|
|
// Start auto-refresh if not already running
|
|
if (!refreshInterval) {
|
|
startAutoRefresh();
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|