Rename WLED Grab to LED Grab, merge Devices into Targets tab with WLED sub-tab, and UI polish
- Rename "WLED Grab" to "LED Grab" across all files (title, logs, locales) - Merge Devices and Targets into a single Targets tab with WLED sub-tab containing Devices and Targets sections (like Sources tab pattern) - Make target card source name label full-width - Render engine template config as two-column key-value grid - Update CLAUDE.md: no server restart needed for frontend-only changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -68,7 +68,9 @@
|
|||||||
|
|
||||||
## IMPORTANT: Auto-Restart Server on Code Changes
|
## IMPORTANT: Auto-Restart Server on Code Changes
|
||||||
|
|
||||||
**Whenever server-side Python code is modified** (any file under `/server/src/`), **automatically restart the server** so the changes take effect immediately. Do NOT wait for the user to ask for a restart.
|
**Whenever server-side Python code is modified** (any file under `/server/src/` **excluding** `/server/src/wled_controller/static/`), **automatically restart the server** so the changes take effect immediately. Do NOT wait for the user to ask for a restart.
|
||||||
|
|
||||||
|
**No restart needed for frontend-only changes.** Files under `/server/src/wled_controller/static/` (HTML, JS, CSS, JSON locale files) are served directly by FastAPI's static file handler — changes take effect on the next browser page refresh without restarting the server.
|
||||||
|
|
||||||
### Restart procedure
|
### Restart procedure
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""WLED Grab - Ambient lighting based on screen content."""
|
"""LED Grab - Ambient lighting based on screen content."""
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.1.0"
|
||||||
__author__ = "Alexei Dolgolyov"
|
__author__ = "Alexei Dolgolyov"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Configuration management for WLED Grab."""
|
"""Configuration management for LED Grab."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ async def lifespan(app: FastAPI):
|
|||||||
Handles startup and shutdown events.
|
Handles startup and shutdown events.
|
||||||
"""
|
"""
|
||||||
# Startup
|
# Startup
|
||||||
logger.info(f"Starting WLED Grab v{__version__}")
|
logger.info(f"Starting LED Grab v{__version__}")
|
||||||
logger.info(f"Python version: {sys.version}")
|
logger.info(f"Python version: {sys.version}")
|
||||||
logger.info(f"Server listening on {config.server.host}:{config.server.port}")
|
logger.info(f"Server listening on {config.server.host}:{config.server.port}")
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ async def lifespan(app: FastAPI):
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
# Shutdown
|
# Shutdown
|
||||||
logger.info("Shutting down WLED Grab")
|
logger.info("Shutting down LED Grab")
|
||||||
|
|
||||||
# Stop all processing
|
# Stop all processing
|
||||||
try:
|
try:
|
||||||
@@ -192,7 +192,7 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
# Create FastAPI application
|
# Create FastAPI application
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="WLED Grab",
|
title="LED Grab",
|
||||||
description="Control WLED devices based on screen content for ambient lighting",
|
description="Control WLED devices based on screen content for ambient lighting",
|
||||||
version=__version__,
|
version=__version__,
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
@@ -246,7 +246,7 @@ async def root():
|
|||||||
|
|
||||||
# Fallback to JSON if static files not found
|
# Fallback to JSON if static files not found
|
||||||
return {
|
return {
|
||||||
"name": "WLED Grab",
|
"name": "LED Grab",
|
||||||
"version": __version__,
|
"version": __version__,
|
||||||
"docs": "/docs",
|
"docs": "/docs",
|
||||||
"health": "/health",
|
"health": "/health",
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ const supportedLocales = {
|
|||||||
|
|
||||||
// Minimal inline fallback for critical UI elements
|
// Minimal inline fallback for critical UI elements
|
||||||
const fallbackTranslations = {
|
const fallbackTranslations = {
|
||||||
'app.title': 'WLED Grab',
|
'app.title': 'LED Grab',
|
||||||
'auth.placeholder': 'Enter your API key...',
|
'auth.placeholder': 'Enter your API key...',
|
||||||
'auth.button.login': 'Login'
|
'auth.button.login': 'Login'
|
||||||
};
|
};
|
||||||
@@ -400,7 +400,7 @@ function updateAllText() {
|
|||||||
// Re-render dynamic content with new translations
|
// Re-render dynamic content with new translations
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
loadDisplays();
|
loadDisplays();
|
||||||
loadDevices();
|
loadTargetsTab();
|
||||||
loadPictureSources();
|
loadPictureSources();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -436,7 +436,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
// User is logged in, load data
|
// User is logged in, load data
|
||||||
loadServerInfo();
|
loadServerInfo();
|
||||||
loadDisplays();
|
loadDisplays();
|
||||||
loadDevices();
|
loadTargetsTab();
|
||||||
|
|
||||||
// Start auto-refresh
|
// Start auto-refresh
|
||||||
startAutoRefresh();
|
startAutoRefresh();
|
||||||
@@ -581,12 +581,14 @@ function switchTab(name) {
|
|||||||
if (name === 'streams') {
|
if (name === 'streams') {
|
||||||
loadPictureSources();
|
loadPictureSources();
|
||||||
} else if (name === 'targets') {
|
} else if (name === 'targets') {
|
||||||
loadTargets();
|
loadTargetsTab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTabs() {
|
function initTabs() {
|
||||||
const saved = localStorage.getItem('activeTab');
|
let saved = localStorage.getItem('activeTab');
|
||||||
|
// Migrate legacy 'devices' tab to 'targets' (devices now live inside targets)
|
||||||
|
if (saved === 'devices') saved = 'targets';
|
||||||
if (saved && document.getElementById(`tab-${saved}`)) {
|
if (saved && document.getElementById(`tab-${saved}`)) {
|
||||||
switchTab(saved);
|
switchTab(saved);
|
||||||
}
|
}
|
||||||
@@ -595,68 +597,8 @@ function initTabs() {
|
|||||||
|
|
||||||
// Load devices
|
// Load devices
|
||||||
async function loadDevices() {
|
async function loadDevices() {
|
||||||
try {
|
// Devices now render inside the combined Targets tab
|
||||||
const response = await fetch(`${API_BASE}/devices`, {
|
await loadTargetsTab();
|
||||||
headers: getHeaders()
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status === 401) {
|
|
||||||
handle401Error();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const devices = data.devices || [];
|
|
||||||
|
|
||||||
const container = document.getElementById('devices-list');
|
|
||||||
|
|
||||||
if (!devices || devices.length === 0) {
|
|
||||||
container.innerHTML = `<div class="template-card add-template-card" onclick="showAddDevice()">
|
|
||||||
<div class="add-template-icon">+</div>
|
|
||||||
</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch state for each device
|
|
||||||
const devicesWithState = await Promise.all(
|
|
||||||
devices.map(async (device) => {
|
|
||||||
try {
|
|
||||||
const stateResponse = await fetch(`${API_BASE}/devices/${device.id}/state`, {
|
|
||||||
headers: getHeaders()
|
|
||||||
});
|
|
||||||
const state = stateResponse.ok ? await stateResponse.json() : {};
|
|
||||||
return { ...device, state };
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to load state for device ${device.id}:`, error);
|
|
||||||
return device;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
container.innerHTML = devicesWithState.map(device => createDeviceCard(device)).join('')
|
|
||||||
+ `<div class="card add-device-card" onclick="showAddDevice()">
|
|
||||||
<div class="add-device-icon">+</div>
|
|
||||||
<div class="add-device-label">${t('devices.add')}</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
// Update footer WLED Web UI link with first device's URL
|
|
||||||
const webuiLink = document.querySelector('.wled-webui-link');
|
|
||||||
if (webuiLink && devicesWithState.length > 0 && devicesWithState[0].url) {
|
|
||||||
webuiLink.href = devicesWithState[0].url;
|
|
||||||
webuiLink.target = '_blank';
|
|
||||||
webuiLink.rel = 'noopener';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach event listeners and fetch real WLED brightness
|
|
||||||
devicesWithState.forEach(device => {
|
|
||||||
attachDeviceListeners(device.id);
|
|
||||||
fetchDeviceBrightness(device.id);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load devices:', error);
|
|
||||||
document.getElementById('devices-list').innerHTML =
|
|
||||||
`<div class="loading">${t('devices.failed')}</div>`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDeviceCard(device) {
|
function createDeviceCard(device) {
|
||||||
@@ -1000,11 +942,9 @@ function startAutoRefresh() {
|
|||||||
refreshInterval = setInterval(() => {
|
refreshInterval = setInterval(() => {
|
||||||
// Only refresh if user is authenticated
|
// Only refresh if user is authenticated
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
const activeTab = localStorage.getItem('activeTab') || 'devices';
|
const activeTab = localStorage.getItem('activeTab') || 'targets';
|
||||||
if (activeTab === 'devices') {
|
if (activeTab === 'targets') {
|
||||||
loadDevices();
|
loadTargetsTab();
|
||||||
} else if (activeTab === 'targets') {
|
|
||||||
loadTargets();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 2000); // Refresh every 2 seconds
|
}, 2000); // Refresh every 2 seconds
|
||||||
@@ -2514,13 +2454,14 @@ async function onEngineChange() {
|
|||||||
configSection.style.display = 'none';
|
configSection.style.display = 'none';
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
let gridHtml = '<div class="config-grid">';
|
||||||
Object.entries(defaultConfig).forEach(([key, value]) => {
|
Object.entries(defaultConfig).forEach(([key, value]) => {
|
||||||
const fieldType = typeof value === 'number' ? 'number' : 'text';
|
const fieldType = typeof value === 'number' ? 'number' : 'text';
|
||||||
const fieldValue = typeof value === 'boolean' ? (value ? 'true' : 'false') : value;
|
const fieldValue = typeof value === 'boolean' ? (value ? 'true' : 'false') : value;
|
||||||
|
|
||||||
const fieldHtml = `
|
gridHtml += `
|
||||||
<div class="form-group">
|
<label class="config-grid-label" for="config-${key}">${key}</label>
|
||||||
<label for="config-${key}">${key}:</label>
|
<div class="config-grid-value">
|
||||||
${typeof value === 'boolean' ? `
|
${typeof value === 'boolean' ? `
|
||||||
<select id="config-${key}" data-config-key="${key}">
|
<select id="config-${key}" data-config-key="${key}">
|
||||||
<option value="true" ${value ? 'selected' : ''}>true</option>
|
<option value="true" ${value ? 'selected' : ''}>true</option>
|
||||||
@@ -2529,11 +2470,11 @@ async function onEngineChange() {
|
|||||||
` : `
|
` : `
|
||||||
<input type="${fieldType}" id="config-${key}" data-config-key="${key}" value="${fieldValue}">
|
<input type="${fieldType}" id="config-${key}" data-config-key="${key}" value="${fieldValue}">
|
||||||
`}
|
`}
|
||||||
<small class="form-hint">${t('templates.config.default')}: ${JSON.stringify(value)}</small>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
configFields.innerHTML += fieldHtml;
|
|
||||||
});
|
});
|
||||||
|
gridHtml += '</div>';
|
||||||
|
configFields.innerHTML = gridHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
configSection.style.display = 'block';
|
configSection.style.display = 'block';
|
||||||
@@ -3998,27 +3939,66 @@ async function saveTargetEditor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== PICTURE TARGETS =====
|
// ===== TARGETS TAB (WLED devices + targets combined) =====
|
||||||
|
|
||||||
async function loadTargets() {
|
async function loadTargets() {
|
||||||
|
// Alias for backward compatibility
|
||||||
|
await loadTargetsTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTargetSubTab(tabKey) {
|
||||||
|
document.querySelectorAll('.target-sub-tab-btn').forEach(btn =>
|
||||||
|
btn.classList.toggle('active', btn.dataset.targetSubTab === tabKey)
|
||||||
|
);
|
||||||
|
document.querySelectorAll('.target-sub-tab-panel').forEach(panel =>
|
||||||
|
panel.classList.toggle('active', panel.id === `target-sub-tab-${tabKey}`)
|
||||||
|
);
|
||||||
|
localStorage.setItem('activeTargetSubTab', tabKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTargetsTab() {
|
||||||
|
const container = document.getElementById('targets-panel-content');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/picture-targets`, { headers: getHeaders() });
|
// Fetch devices, targets, and sources in parallel
|
||||||
|
const [devicesResp, targetsResp, sourcesResp] = await Promise.all([
|
||||||
|
fetch(`${API_BASE}/devices`, { headers: getHeaders() }),
|
||||||
|
fetch(`${API_BASE}/picture-targets`, { headers: getHeaders() }),
|
||||||
|
fetchWithAuth('/picture-sources').catch(() => null),
|
||||||
|
]);
|
||||||
|
|
||||||
if (response.status === 401) { handle401Error(); return; }
|
if (devicesResp.status === 401 || targetsResp.status === 401) {
|
||||||
|
handle401Error();
|
||||||
const data = await response.json();
|
|
||||||
const targets = data.targets || [];
|
|
||||||
|
|
||||||
const container = document.getElementById('targets-list');
|
|
||||||
|
|
||||||
if (!targets || targets.length === 0) {
|
|
||||||
container.innerHTML = `<div class="template-card add-template-card" onclick="showTargetEditor()">
|
|
||||||
<div class="add-template-icon">+</div>
|
|
||||||
</div>`;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch state for each target
|
const devicesData = await devicesResp.json();
|
||||||
|
const devices = devicesData.devices || [];
|
||||||
|
|
||||||
|
const targetsData = await targetsResp.json();
|
||||||
|
const targets = targetsData.targets || [];
|
||||||
|
|
||||||
|
let sourceMap = {};
|
||||||
|
if (sourcesResp && sourcesResp.ok) {
|
||||||
|
const srcData = await sourcesResp.json();
|
||||||
|
(srcData.streams || []).forEach(s => { sourceMap[s.id] = s; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch state for each device
|
||||||
|
const devicesWithState = await Promise.all(
|
||||||
|
devices.map(async (device) => {
|
||||||
|
try {
|
||||||
|
const stateResp = await fetch(`${API_BASE}/devices/${device.id}/state`, { headers: getHeaders() });
|
||||||
|
const state = stateResp.ok ? await stateResp.json() : {};
|
||||||
|
return { ...device, state };
|
||||||
|
} catch {
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch state + metrics for each target
|
||||||
const targetsWithState = await Promise.all(
|
const targetsWithState = await Promise.all(
|
||||||
targets.map(async (target) => {
|
targets.map(async (target) => {
|
||||||
try {
|
try {
|
||||||
@@ -4033,31 +4013,58 @@ async function loadTargets() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Also fetch devices and sources for name resolution
|
// Build device map for target name resolution
|
||||||
let deviceMap = {};
|
const deviceMap = {};
|
||||||
let sourceMap = {};
|
devicesWithState.forEach(d => { deviceMap[d.id] = d; });
|
||||||
try {
|
|
||||||
const devResp = await fetch(`${API_BASE}/devices`, { headers: getHeaders() });
|
|
||||||
if (devResp.ok) {
|
|
||||||
const devData = await devResp.json();
|
|
||||||
(devData.devices || []).forEach(d => { deviceMap[d.id] = d; });
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
try {
|
|
||||||
const srcResp = await fetchWithAuth('/picture-sources');
|
|
||||||
if (srcResp.ok) {
|
|
||||||
const srcData = await srcResp.json();
|
|
||||||
(srcData.streams || []).forEach(s => { sourceMap[s.id] = s; });
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
container.innerHTML = targetsWithState.map(target => createTargetCard(target, deviceMap, sourceMap)).join('')
|
// Group by type (currently only WLED)
|
||||||
+ `<div class="template-card add-template-card" onclick="showTargetEditor()">
|
const wledDevices = devicesWithState;
|
||||||
|
const wledTargets = targetsWithState.filter(t => t.target_type === 'wled');
|
||||||
|
|
||||||
|
const activeSubTab = localStorage.getItem('activeTargetSubTab') || 'wled';
|
||||||
|
|
||||||
|
const subTabs = [
|
||||||
|
{ key: 'wled', icon: '💡', titleKey: 'targets.subtab.wled', count: wledDevices.length + wledTargets.length },
|
||||||
|
];
|
||||||
|
|
||||||
|
const tabBar = `<div class="stream-tab-bar">${subTabs.map(tab =>
|
||||||
|
`<button class="target-sub-tab-btn stream-tab-btn${tab.key === activeSubTab ? ' active' : ''}" data-target-sub-tab="${tab.key}" onclick="switchTargetSubTab('${tab.key}')">${tab.icon} ${t(tab.titleKey)} <span class="stream-tab-count">${tab.count}</span></button>`
|
||||||
|
).join('')}</div>`;
|
||||||
|
|
||||||
|
// WLED panel: devices section + targets section
|
||||||
|
const wledPanel = `
|
||||||
|
<div class="target-sub-tab-panel stream-tab-panel${activeSubTab === 'wled' ? ' active' : ''}" id="target-sub-tab-wled">
|
||||||
|
<div class="subtab-section">
|
||||||
|
<h3 class="subtab-section-header">${t('targets.section.devices')}</h3>
|
||||||
|
<div class="devices-grid">
|
||||||
|
${wledDevices.map(device => createDeviceCard(device)).join('')}
|
||||||
|
<div class="template-card add-template-card" onclick="showAddDevice()">
|
||||||
<div class="add-template-icon">+</div>
|
<div class="add-template-icon">+</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="subtab-section">
|
||||||
|
<h3 class="subtab-section-header">${t('targets.section.targets')}</h3>
|
||||||
|
<div class="devices-grid">
|
||||||
|
${wledTargets.map(target => createTargetCard(target, deviceMap, sourceMap)).join('')}
|
||||||
|
<div class="template-card add-template-card" onclick="showTargetEditor()">
|
||||||
|
<div class="add-template-icon">+</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
|
container.innerHTML = tabBar + wledPanel;
|
||||||
|
|
||||||
|
// Attach event listeners and fetch WLED brightness for device cards
|
||||||
|
devicesWithState.forEach(device => {
|
||||||
|
attachDeviceListeners(device.id);
|
||||||
|
fetchDeviceBrightness(device.id);
|
||||||
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load targets:', error);
|
console.error('Failed to load targets tab:', error);
|
||||||
document.getElementById('targets-list').innerHTML = `<div class="loading">${t('targets.failed')}</div>`;
|
container.innerHTML = `<div class="loading">${t('targets.failed')}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4095,7 +4102,7 @@ function createTargetCard(target, deviceMap, sourceMap) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="stream-card-props">
|
<div class="stream-card-props">
|
||||||
<span class="stream-card-prop" title="${t('targets.device')}">💡 ${escapeHtml(deviceName)}</span>
|
<span class="stream-card-prop" title="${t('targets.device')}">💡 ${escapeHtml(deviceName)}</span>
|
||||||
<span class="stream-card-prop" title="${t('targets.source')}">📺 ${escapeHtml(sourceName)}</span>
|
<span class="stream-card-prop stream-card-prop-full" title="${t('targets.source')}">📺 ${escapeHtml(sourceName)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
${isProcessing ? `
|
${isProcessing ? `
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>WLED Grab</title>
|
<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="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">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
</head>
|
</head>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<header>
|
<header>
|
||||||
<div class="header-title">
|
<div class="header-title">
|
||||||
<span id="server-status" class="status-badge">●</span>
|
<span id="server-status" class="status-badge">●</span>
|
||||||
<h1 data-i18n="app.title">WLED Grab</h1>
|
<h1 data-i18n="app.title">LED Grab</h1>
|
||||||
<span id="server-version"><span id="version-number"></span></span>
|
<span id="server-version"><span id="version-number"></span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="server-info">
|
<div class="server-info">
|
||||||
@@ -34,19 +34,12 @@
|
|||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div class="tab-bar">
|
<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 active" 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>
|
<button class="tab-btn" data-tab="streams" onclick="switchTab('streams')"><span data-i18n="streams.title">📺 Sources</span></button>
|
||||||
<button class="tab-btn" data-tab="targets" onclick="switchTab('targets')"><span data-i18n="targets.title">⚡ Targets</span></button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-panel active" id="tab-devices">
|
<div class="tab-panel active" id="tab-targets">
|
||||||
<div id="devices-list" class="devices-grid">
|
<div id="targets-panel-content">
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-panel" id="tab-targets">
|
|
||||||
<div id="targets-list" class="devices-grid">
|
|
||||||
<div class="loading-spinner"></div>
|
<div class="loading-spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -321,7 +314,7 @@
|
|||||||
<form id="api-key-form" onsubmit="submitApiKey(event)">
|
<form id="api-key-form" onsubmit="submitApiKey(event)">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p class="modal-description" data-i18n="auth.message">
|
<p class="modal-description" data-i18n="auth.message">
|
||||||
Please enter your API key to authenticate and access the WLED Grab.
|
Please enter your API key to authenticate and access the LED Grab.
|
||||||
</p>
|
</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="label-row">
|
<div class="label-row">
|
||||||
@@ -761,7 +754,7 @@
|
|||||||
showToast(t('auth.logout.success'), 'info');
|
showToast(t('auth.logout.success'), 'info');
|
||||||
|
|
||||||
// Clear the UI
|
// Clear the UI
|
||||||
document.getElementById('devices-list').innerHTML = `<div class="loading">${t('auth.please_login')} devices</div>`;
|
document.getElementById('targets-panel-content').innerHTML = `<div class="loading">${t('auth.please_login')}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on load
|
// Initialize on load
|
||||||
@@ -837,7 +830,7 @@
|
|||||||
// Reload data
|
// Reload data
|
||||||
loadServerInfo();
|
loadServerInfo();
|
||||||
loadDisplays();
|
loadDisplays();
|
||||||
loadDevices();
|
loadTargetsTab();
|
||||||
|
|
||||||
// Start auto-refresh if not already running
|
// Start auto-refresh if not already running
|
||||||
if (!refreshInterval) {
|
if (!refreshInterval) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"app.title": "WLED Grab",
|
"app.title": "LED Grab",
|
||||||
"app.version": "Version:",
|
"app.version": "Version:",
|
||||||
"theme.toggle": "Toggle theme",
|
"theme.toggle": "Toggle theme",
|
||||||
"locale.change": "Change language",
|
"locale.change": "Change language",
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
"auth.logout": "Logout",
|
"auth.logout": "Logout",
|
||||||
"auth.authenticated": "● Authenticated",
|
"auth.authenticated": "● Authenticated",
|
||||||
"auth.title": "Login to WLED Controller",
|
"auth.title": "Login to WLED Controller",
|
||||||
"auth.message": "Please enter your API key to authenticate and access the WLED Grab.",
|
"auth.message": "Please enter your API key to authenticate and access the LED Grab.",
|
||||||
"auth.label": "API Key:",
|
"auth.label": "API Key:",
|
||||||
"auth.placeholder": "Enter your API key...",
|
"auth.placeholder": "Enter your API key...",
|
||||||
"auth.hint": "Your API key will be stored securely in your browser's local storage.",
|
"auth.hint": "Your API key will be stored securely in your browser's local storage.",
|
||||||
@@ -301,6 +301,9 @@
|
|||||||
"streams.validate_image.invalid": "Image not accessible",
|
"streams.validate_image.invalid": "Image not accessible",
|
||||||
"targets.title": "⚡ Targets",
|
"targets.title": "⚡ Targets",
|
||||||
"targets.description": "Targets bridge picture sources to output devices. Each target references a device and a source, with its own processing settings.",
|
"targets.description": "Targets bridge picture sources to output devices. Each target references a device and a source, with its own processing settings.",
|
||||||
|
"targets.subtab.wled": "WLED",
|
||||||
|
"targets.section.devices": "💡 Devices",
|
||||||
|
"targets.section.targets": "⚡ Targets",
|
||||||
"targets.add": "Add Target",
|
"targets.add": "Add Target",
|
||||||
"targets.edit": "Edit Target",
|
"targets.edit": "Edit Target",
|
||||||
"targets.loading": "Loading targets...",
|
"targets.loading": "Loading targets...",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"app.title": "WLED Grab",
|
"app.title": "LED Grab",
|
||||||
"app.version": "Версия:",
|
"app.version": "Версия:",
|
||||||
"theme.toggle": "Переключить тему",
|
"theme.toggle": "Переключить тему",
|
||||||
"locale.change": "Изменить язык",
|
"locale.change": "Изменить язык",
|
||||||
"auth.login": "Войти",
|
"auth.login": "Войти",
|
||||||
"auth.logout": "Выйти",
|
"auth.logout": "Выйти",
|
||||||
"auth.authenticated": "● Авторизован",
|
"auth.authenticated": "● Авторизован",
|
||||||
"auth.title": "Вход в WLED Grab",
|
"auth.title": "Вход в LED Grab",
|
||||||
"auth.message": "Пожалуйста, введите ваш API ключ для аутентификации и доступа к WLED Grab.",
|
"auth.message": "Пожалуйста, введите ваш API ключ для аутентификации и доступа к LED Grab.",
|
||||||
"auth.label": "API Ключ:",
|
"auth.label": "API Ключ:",
|
||||||
"auth.placeholder": "Введите ваш API ключ...",
|
"auth.placeholder": "Введите ваш API ключ...",
|
||||||
"auth.hint": "Ваш API ключ будет безопасно сохранен в локальном хранилище браузера.",
|
"auth.hint": "Ваш API ключ будет безопасно сохранен в локальном хранилище браузера.",
|
||||||
@@ -301,6 +301,9 @@
|
|||||||
"streams.validate_image.invalid": "Изображение недоступно",
|
"streams.validate_image.invalid": "Изображение недоступно",
|
||||||
"targets.title": "⚡ Цели",
|
"targets.title": "⚡ Цели",
|
||||||
"targets.description": "Цели связывают источники изображений с устройствами вывода. Каждая цель ссылается на устройство и источник, с собственными настройками обработки.",
|
"targets.description": "Цели связывают источники изображений с устройствами вывода. Каждая цель ссылается на устройство и источник, с собственными настройками обработки.",
|
||||||
|
"targets.subtab.wled": "WLED",
|
||||||
|
"targets.section.devices": "💡 Устройства",
|
||||||
|
"targets.section.targets": "⚡ Цели",
|
||||||
"targets.add": "Добавить Цель",
|
"targets.add": "Добавить Цель",
|
||||||
"targets.edit": "Редактировать Цель",
|
"targets.edit": "Редактировать Цель",
|
||||||
"targets.loading": "Загрузка целей...",
|
"targets.loading": "Загрузка целей...",
|
||||||
|
|||||||
@@ -1934,6 +1934,32 @@ input:-webkit-autofill:focus {
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Engine config grid (property name left, input right) */
|
||||||
|
.config-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 8px 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-grid-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-grid-value input,
|
||||||
|
.config-grid-value select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
.template-card-actions {
|
.template-card-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
Reference in New Issue
Block a user