Split monolithic index.html and style.css for maintainability
- Extract 15 modals and 3 partials from index.html into Jinja2 templates (templates/modals/*.html, templates/partials/*.html) - Split style.css (3,712 lines) into 11 feature-scoped CSS files under static/css/ (base, layout, components, cards, modal, calibration, dashboard, streams, patterns, profiles, tutorials) - Switch root route from FileResponse to Jinja2Templates - Add jinja2 dependency - Consolidate duplicate @keyframes spin definition - Browser receives identical assembled HTML — zero JS changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
276
server/src/wled_controller/templates/index.html
Normal file
276
server/src/wled_controller/templates/index.html
Normal file
@@ -0,0 +1,276 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LED Grab</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/css/base.css">
|
||||
<link rel="stylesheet" href="/static/css/layout.css">
|
||||
<link rel="stylesheet" href="/static/css/components.css">
|
||||
<link rel="stylesheet" href="/static/css/cards.css">
|
||||
<link rel="stylesheet" href="/static/css/modal.css">
|
||||
<link rel="stylesheet" href="/static/css/calibration.css">
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
<link rel="stylesheet" href="/static/css/streams.css">
|
||||
<link rel="stylesheet" href="/static/css/patterns.css">
|
||||
<link rel="stylesheet" href="/static/css/profiles.css">
|
||||
<link rel="stylesheet" href="/static/css/tutorials.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
||||
</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">LED Grab</h1>
|
||||
<span id="server-version"><span id="version-number"></span></span>
|
||||
</div>
|
||||
<div class="server-info">
|
||||
<a href="/docs" target="_blank" class="header-link" data-i18n-title="app.api_docs" title="API Docs">API</a>
|
||||
<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;">
|
||||
🚪
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab-bar">
|
||||
<button class="tab-btn" data-tab="dashboard" onclick="switchTab('dashboard')"><span data-i18n="dashboard.title">📊 Dashboard</span></button>
|
||||
<button class="tab-btn" data-tab="profiles" onclick="switchTab('profiles')"><span data-i18n="profiles.title">📋 Profiles</span></button>
|
||||
<button class="tab-btn" data-tab="targets" onclick="switchTab('targets')"><span data-i18n="targets.title">⚡ Targets</span></button>
|
||||
<button class="tab-btn" data-tab="streams" onclick="switchTab('streams')"><span data-i18n="streams.title">📺 Sources</span></button>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-dashboard">
|
||||
<div id="dashboard-content">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-profiles">
|
||||
<div id="profiles-content">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-targets">
|
||||
<div id="targets-panel-content">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-streams">
|
||||
<div id="streams-list">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Apply saved tab immediately during parse to prevent visible jump
|
||||
(function() {
|
||||
var saved = localStorage.getItem('activeTab');
|
||||
if (saved === 'devices') saved = 'targets';
|
||||
if (!saved || !document.getElementById('tab-' + saved)) saved = 'dashboard';
|
||||
document.querySelectorAll('.tab-btn').forEach(function(btn) { btn.classList.toggle('active', btn.dataset.tab === saved); });
|
||||
document.querySelectorAll('.tab-panel').forEach(function(panel) { panel.classList.toggle('active', panel.id === 'tab-' + saved); });
|
||||
})();
|
||||
</script>
|
||||
</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>
|
||||
|
||||
{% include 'modals/calibration.html' %}
|
||||
{% include 'modals/device-settings.html' %}
|
||||
{% include 'modals/target-editor.html' %}
|
||||
{% include 'modals/kc-editor.html' %}
|
||||
{% include 'modals/pattern-template.html' %}
|
||||
{% include 'modals/api-key.html' %}
|
||||
{% include 'modals/confirm.html' %}
|
||||
{% include 'modals/add-device.html' %}
|
||||
{% include 'modals/capture-template.html' %}
|
||||
{% include 'modals/test-template.html' %}
|
||||
{% include 'modals/test-stream.html' %}
|
||||
{% include 'modals/test-pp-template.html' %}
|
||||
{% include 'modals/stream.html' %}
|
||||
{% include 'modals/pp-template.html' %}
|
||||
{% include 'modals/profile-editor.html' %}
|
||||
|
||||
{% include 'partials/tutorial-overlay.html' %}
|
||||
{% include 'partials/image-lightbox.html' %}
|
||||
{% include 'partials/display-picker.html' %}
|
||||
|
||||
<script type="module" src="/static/js/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 tabBar = document.querySelector('.tab-bar');
|
||||
|
||||
if (apiKey) {
|
||||
loginBtn.style.display = 'none';
|
||||
logoutBtn.style.display = 'inline-block';
|
||||
if (tabBar) tabBar.style.display = '';
|
||||
} else {
|
||||
loginBtn.style.display = 'inline-block';
|
||||
logoutBtn.style.display = 'none';
|
||||
if (tabBar) tabBar.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');
|
||||
if (window.setApiKey) window.setApiKey(null);
|
||||
updateAuthUI();
|
||||
showToast(t('auth.logout.success'), 'info');
|
||||
|
||||
// Stop background activity
|
||||
if (window.stopDashboardWS) window.stopDashboardWS();
|
||||
if (window.stopPerfPolling) window.stopPerfPolling();
|
||||
if (window.stopUptimeTimer) window.stopUptimeTimer();
|
||||
if (window.disconnectAllKCWebSockets) window.disconnectAllKCWebSockets();
|
||||
|
||||
// Clear all tab panels
|
||||
const loginMsg = `<div class="loading">${t('auth.please_login')}</div>`;
|
||||
document.getElementById('dashboard-content').innerHTML = loginMsg;
|
||||
document.getElementById('profiles-content').innerHTML = loginMsg;
|
||||
document.getElementById('targets-panel-content').innerHTML = loginMsg;
|
||||
document.getElementById('streams-list').innerHTML = loginMsg;
|
||||
}
|
||||
|
||||
// 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');
|
||||
|
||||
description.textContent = message || t('auth.message');
|
||||
|
||||
input.value = '';
|
||||
input.placeholder = t('auth.placeholder');
|
||||
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' : '';
|
||||
|
||||
// Hide login button while modal is open
|
||||
document.getElementById('login-btn').style.display = 'none';
|
||||
|
||||
setTimeout(() => input.focus(), 100);
|
||||
}
|
||||
|
||||
function closeApiKeyModal() {
|
||||
const modal = document.getElementById('api-key-modal');
|
||||
modal.style.display = 'none';
|
||||
unlockBody();
|
||||
updateAuthUI();
|
||||
}
|
||||
|
||||
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);
|
||||
if (window.setApiKey) window.setApiKey(key);
|
||||
updateAuthUI();
|
||||
|
||||
closeApiKeyModal();
|
||||
showToast(t('auth.success'), 'success');
|
||||
|
||||
// Reload data
|
||||
loadServerInfo();
|
||||
loadDisplays();
|
||||
loadTargetsTab();
|
||||
|
||||
// Start auto-refresh
|
||||
startAutoRefresh();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user