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 ` -
- -
-
- ${engineIcon} ${escapeHtml(template.name)} -
-
-
- ⚙️ ${template.engine_type.toUpperCase()} - ${configEntries.length > 0 ? `🔧 ${configEntries.length}` : ''} -
- ${configEntries.length > 0 ? ` -
- ${t('templates.config.show')} - - ${configEntries.map(([key, val]) => ` - - - - - `).join('')} -
${escapeHtml(key)}${escapeHtml(String(val))}
-
- ` : ''} -
- - -
-
- `; - }; - - 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 ` +
+ +
+
+ ${engineIcon} ${escapeHtml(template.name)} +
+
+
+ ⚙️ ${template.engine_type.toUpperCase()} + ${configEntries.length > 0 ? `🔧 ${configEntries.length}` : ''} +
+ ${configEntries.length > 0 ? ` +
+ ${t('templates.config.show')} + + ${configEntries.map(([key, val]) => ` + + + + + `).join('')} +
${escapeHtml(key)}${escapeHtml(String(val))}
+
+ ` : ''} +
+ + +
+
+ `; + }; + + 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 ` +
+ +
+
+ 🎨 ${escapeHtml(tmpl.name)} +
+
+ ${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) => `
+
-
${t(labelKey)}
`; 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 = ` +
+

${t('streams.section.streams')}

+
+ ${tab.streams.map(renderStreamCard).join('')} + ${addStreamCard(tab.key)} +
+
+
+

${t('templates.title')}

+
+ ${_cachedCaptureTemplates.map(renderCaptureTemplateCard).join('')} +
+
+
+
+
+
`; + } else if (tab.key === 'processed') { + // Processed: streams section + PP templates section + panelContent = ` +
+

${t('streams.section.streams')}

+
+ ${tab.streams.map(renderStreamCard).join('')} + ${addStreamCard(tab.key)} +
+
+
+

${t('postprocessing.title')}

+
+ ${_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 ` -
- -
-
- 🎨 ${escapeHtml(tmpl.name)} -
-
- ${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 @@
-

WLED Screen Controller

+

WLED Grab

@@ -36,9 +36,7 @@
- - - +
@@ -54,17 +52,6 @@
-
-
-
-
-
- -
-
-
-
-