diff --git a/server/src/wled_controller/__init__.py b/server/src/wled_controller/__init__.py
index 13a4c41..128f359 100644
--- a/server/src/wled_controller/__init__.py
+++ b/server/src/wled_controller/__init__.py
@@ -1,4 +1,4 @@
-"""WLED Screen Controller - Ambient lighting based on screen content."""
+"""WLED Grab - Ambient lighting based on screen content."""
__version__ = "0.1.0"
__author__ = "Alexei Dolgolyov"
diff --git a/server/src/wled_controller/config.py b/server/src/wled_controller/config.py
index 8ef7092..b938e6d 100644
--- a/server/src/wled_controller/config.py
+++ b/server/src/wled_controller/config.py
@@ -1,4 +1,4 @@
-"""Configuration management for WLED Screen Controller."""
+"""Configuration management for WLED Grab."""
import os
from pathlib import Path
diff --git a/server/src/wled_controller/core/capture_engines/base.py b/server/src/wled_controller/core/capture_engines/base.py
index 490bc7b..c4f3ce8 100644
--- a/server/src/wled_controller/core/capture_engines/base.py
+++ b/server/src/wled_controller/core/capture_engines/base.py
@@ -35,7 +35,7 @@ class CaptureEngine(ABC):
"""Abstract base class for screen capture engines.
All screen capture engines must implement this interface to be
- compatible with the WLED Screen Controller system.
+ compatible with the WLED Grab system.
"""
ENGINE_TYPE: str = "base" # Override in subclasses
diff --git a/server/src/wled_controller/main.py b/server/src/wled_controller/main.py
index 5821e42..a5108a5 100644
--- a/server/src/wled_controller/main.py
+++ b/server/src/wled_controller/main.py
@@ -98,7 +98,7 @@ async def lifespan(app: FastAPI):
Handles startup and shutdown events.
"""
# Startup
- logger.info(f"Starting WLED Screen Controller v{__version__}")
+ logger.info(f"Starting WLED Grab v{__version__}")
logger.info(f"Python version: {sys.version}")
logger.info(f"Server listening on {config.server.host}:{config.server.port}")
@@ -152,7 +152,7 @@ async def lifespan(app: FastAPI):
yield
# Shutdown
- logger.info("Shutting down WLED Screen Controller")
+ logger.info("Shutting down WLED Grab")
# Stop all processing
try:
@@ -163,7 +163,7 @@ async def lifespan(app: FastAPI):
# Create FastAPI application
app = FastAPI(
- title="WLED Screen Controller",
+ title="WLED Grab",
description="Control WLED devices based on screen content for ambient lighting",
version=__version__,
lifespan=lifespan,
@@ -217,7 +217,7 @@ async def root():
# Fallback to JSON if static files not found
return {
- "name": "WLED Screen Controller",
+ "name": "WLED Grab",
"version": __version__,
"docs": "/docs",
"health": "/health",
diff --git a/server/src/wled_controller/static/app.js b/server/src/wled_controller/static/app.js
index 1d7b535..afb94e1 100644
--- a/server/src/wled_controller/static/app.js
+++ b/server/src/wled_controller/static/app.js
@@ -208,7 +208,7 @@ const supportedLocales = {
// Minimal inline fallback for critical UI elements
const fallbackTranslations = {
- 'app.title': 'WLED Screen Controller',
+ 'app.title': 'WLED Grab',
'auth.placeholder': 'Enter your API key...',
'auth.button.login': 'Login'
};
@@ -494,18 +494,16 @@ async function loadDisplays() {
let _cachedDisplays = null;
function switchTab(name) {
+ // Migrate legacy tab values from localStorage
+ if (name === 'templates' || name === 'pp-templates') {
+ name = 'streams';
+ }
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.tab === name));
document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.toggle('active', panel.id === `tab-${name}`));
localStorage.setItem('activeTab', name);
- if (name === 'templates') {
- loadCaptureTemplates();
- }
if (name === 'streams') {
loadPictureStreams();
}
- if (name === 'pp-templates') {
- loadPPTemplates();
- }
}
function initTabs() {
@@ -2423,80 +2421,15 @@ async function loadCaptureTemplates() {
if (!response.ok) {
throw new Error(`Failed to load templates: ${response.status}`);
}
-
const data = await response.json();
- renderTemplatesList(data.templates || []);
+ _cachedCaptureTemplates = data.templates || [];
+ // Re-render the streams tab which now contains template sections
+ renderPictureStreamsList(_cachedStreams);
} catch (error) {
console.error('Error loading capture templates:', error);
- document.getElementById('templates-list').innerHTML = `
-
${t('templates.error.load')}: ${error.message}
- `;
}
}
-// Render templates list
-function renderTemplatesList(templates) {
- const container = document.getElementById('templates-list');
-
- if (templates.length === 0) {
- container.innerHTML = `
-
+
-
${t('templates.add')}
-
`;
- return;
- }
-
- const renderCard = (template) => {
- const engineIcon = getEngineIcon(template.engine_type);
- const configEntries = Object.entries(template.engine_config);
-
- return `
-
-
-
-
- ⚙️ ${template.engine_type.toUpperCase()}
- ${configEntries.length > 0 ? `🔧 ${configEntries.length}` : ''}
-
- ${configEntries.length > 0 ? `
-
- ${t('templates.config.show')}
-
- ${configEntries.map(([key, val]) => `
-
- | ${escapeHtml(key)} |
- ${escapeHtml(String(val))} |
-
- `).join('')}
-
-
- ` : ''}
-
-
-
-
-
- `;
- };
-
- let html = templates.map(renderCard).join('');
-
- html += `
-
+
-
${t('templates.add')}
-
`;
-
- container.innerHTML = html;
-}
-
// Get engine icon
function getEngineIcon(engineType) {
return '🖥️';
@@ -3055,28 +2988,31 @@ let _availableFilters = []; // Loaded from GET /filters
async function loadPictureStreams() {
try {
- // Ensure PP templates and capture templates are cached for stream card display
- if (_cachedPPTemplates.length === 0 || _cachedCaptureTemplates.length === 0) {
- try {
- if (_availableFilters.length === 0) {
- const fr = await fetchWithAuth('/filters');
- if (fr.ok) { const fd = await fr.json(); _availableFilters = fd.filters || []; }
- }
- if (_cachedPPTemplates.length === 0) {
- const pr = await fetchWithAuth('/postprocessing-templates');
- if (pr.ok) { const pd = await pr.json(); _cachedPPTemplates = pd.templates || []; }
- }
- if (_cachedCaptureTemplates.length === 0) {
- const cr = await fetchWithAuth('/capture-templates');
- if (cr.ok) { const cd = await cr.json(); _cachedCaptureTemplates = cd.templates || []; }
- }
- } catch (e) { console.warn('Could not pre-load templates for streams:', e); }
+ // Always fetch templates, filters, and streams in parallel
+ // since templates are now rendered inside stream sub-tabs
+ const [filtersResp, ppResp, captResp, streamsResp] = await Promise.all([
+ _availableFilters.length === 0 ? fetchWithAuth('/filters') : Promise.resolve(null),
+ fetchWithAuth('/postprocessing-templates'),
+ fetchWithAuth('/capture-templates'),
+ fetchWithAuth('/picture-streams')
+ ]);
+
+ if (filtersResp && filtersResp.ok) {
+ const fd = await filtersResp.json();
+ _availableFilters = fd.filters || [];
}
- const response = await fetchWithAuth('/picture-streams');
- if (!response.ok) {
- throw new Error(`Failed to load streams: ${response.status}`);
+ if (ppResp.ok) {
+ const pd = await ppResp.json();
+ _cachedPPTemplates = pd.templates || [];
}
- const data = await response.json();
+ if (captResp.ok) {
+ const cd = await captResp.json();
+ _cachedCaptureTemplates = cd.templates || [];
+ }
+ if (!streamsResp.ok) {
+ throw new Error(`Failed to load streams: ${streamsResp.status}`);
+ }
+ const data = await streamsResp.json();
_cachedStreams = data.streams || [];
renderPictureStreamsList(_cachedStreams);
} catch (error) {
@@ -3101,7 +3037,7 @@ function renderPictureStreamsList(streams) {
const container = document.getElementById('streams-list');
const activeTab = localStorage.getItem('activeStreamTab') || 'raw';
- const renderCard = (stream) => {
+ const renderStreamCard = (stream) => {
const typeIcons = { raw: '🖥️', processed: '🎨', static_image: '🖼️' };
const typeIcon = typeIcons[stream.stream_type] || '📺';
@@ -3158,34 +3094,145 @@ function renderPictureStreamsList(streams) {
`;
};
+ const renderCaptureTemplateCard = (template) => {
+ const engineIcon = getEngineIcon(template.engine_type);
+ const configEntries = Object.entries(template.engine_config);
+ return `
+
+
+
+
+ ⚙️ ${template.engine_type.toUpperCase()}
+ ${configEntries.length > 0 ? `🔧 ${configEntries.length}` : ''}
+
+ ${configEntries.length > 0 ? `
+
+ ${t('templates.config.show')}
+
+ ${configEntries.map(([key, val]) => `
+
+ | ${escapeHtml(key)} |
+ ${escapeHtml(String(val))} |
+
+ `).join('')}
+
+
+ ` : ''}
+
+
+
+
+
+ `;
+ };
+
+ const renderPPTemplateCard = (tmpl) => {
+ let filterChainHtml = '';
+ if (tmpl.filters && tmpl.filters.length > 0) {
+ const filterNames = tmpl.filters.map(fi => `${escapeHtml(_getFilterName(fi.filter_id))}`);
+ filterChainHtml = `${filterNames.join('→')}
`;
+ }
+ return `
+
+
+
+ ${tmpl.description ? `
${escapeHtml(tmpl.description)}
` : ''}
+ ${filterChainHtml}
+
+
+
+
+
+ `;
+ };
+
const rawStreams = streams.filter(s => s.stream_type === 'raw');
const processedStreams = streams.filter(s => s.stream_type === 'processed');
const staticImageStreams = streams.filter(s => s.stream_type === 'static_image');
- const addCard = (type, labelKey) => `
+ const addStreamCard = (type) => `
`;
const tabs = [
- { key: 'raw', icon: '🖥️', titleKey: 'streams.group.raw', streams: rawStreams, addLabelKey: 'streams.add.raw' },
- { key: 'static_image', icon: '🖼️', titleKey: 'streams.group.static_image', streams: staticImageStreams, addLabelKey: 'streams.add.static_image' },
- { key: 'processed', icon: '🎨', titleKey: 'streams.group.processed', streams: processedStreams, addLabelKey: 'streams.add.processed' },
+ { key: 'raw', icon: '🖥️', titleKey: 'streams.group.raw', streams: rawStreams },
+ { key: 'static_image', icon: '🖼️', titleKey: 'streams.group.static_image', streams: staticImageStreams },
+ { key: 'processed', icon: '🎨', titleKey: 'streams.group.processed', streams: processedStreams },
];
const tabBar = `${tabs.map(tab =>
``
).join('')}
`;
- const panels = tabs.map(tab =>
- `
-
- ${tab.streams.map(renderCard).join('')}
- ${addCard(tab.key, tab.addLabelKey)}
-
-
`
- ).join('');
+ const panels = tabs.map(tab => {
+ let panelContent = '';
+
+ if (tab.key === 'raw') {
+ // Screen Capture: streams section + capture templates section
+ panelContent = `
+
+
+
+ ${tab.streams.map(renderStreamCard).join('')}
+ ${addStreamCard(tab.key)}
+
+
+
+
+
+ ${_cachedCaptureTemplates.map(renderCaptureTemplateCard).join('')}
+
+
+
`;
+ } else if (tab.key === 'processed') {
+ // Processed: streams section + PP templates section
+ panelContent = `
+
+
+
+ ${tab.streams.map(renderStreamCard).join('')}
+ ${addStreamCard(tab.key)}
+
+
+
+
+
+ ${_cachedPPTemplates.map(renderPPTemplateCard).join('')}
+
+
+
`;
+ } else {
+ // Static Image: just the stream cards, no section headers
+ panelContent = `
+
+ ${tab.streams.map(renderStreamCard).join('')}
+ ${addStreamCard(tab.key)}
+
`;
+ }
+
+ return `${panelContent}
`;
+ }).join('');
container.innerHTML = tabBar + panels;
}
@@ -3671,12 +3718,10 @@ async function loadPPTemplates() {
}
const data = await response.json();
_cachedPPTemplates = data.templates || [];
- renderPPTemplatesList(_cachedPPTemplates);
+ // Re-render the streams tab which now contains template sections
+ renderPictureStreamsList(_cachedStreams);
} catch (error) {
console.error('Error loading PP templates:', error);
- document.getElementById('pp-templates-list').innerHTML = `
- ${t('postprocessing.error.load')}: ${error.message}
- `;
}
}
@@ -3691,56 +3736,6 @@ function _getFilterName(filterId) {
return translated;
}
-function renderPPTemplatesList(templates) {
- const container = document.getElementById('pp-templates-list');
-
- if (templates.length === 0) {
- container.innerHTML = `
-
+
-
${t('postprocessing.add')}
-
`;
- return;
- }
-
- const renderCard = (tmpl) => {
- // Build filter chain pills
- let filterChainHtml = '';
- if (tmpl.filters && tmpl.filters.length > 0) {
- const filterNames = tmpl.filters.map(fi => `${escapeHtml(_getFilterName(fi.filter_id))}`);
- filterChainHtml = `${filterNames.join('→')}
`;
- }
-
- return `
-
-
-
- ${tmpl.description ? `
${escapeHtml(tmpl.description)}
` : ''}
- ${filterChainHtml}
-
-
-
-
-
- `;
- };
-
- let html = templates.map(renderCard).join('');
- html += `
-
+
-
${t('postprocessing.add')}
-
`;
-
- container.innerHTML = html;
-}
-
// --- Filter list management in PP template modal ---
let _modalFilters = []; // Current filter list being edited in modal
diff --git a/server/src/wled_controller/static/index.html b/server/src/wled_controller/static/index.html
index 2c0af33..0b5f434 100644
--- a/server/src/wled_controller/static/index.html
+++ b/server/src/wled_controller/static/index.html
@@ -3,7 +3,7 @@
- WLED Screen Controller
+ WLED Grab
@@ -12,7 +12,7 @@