diff --git a/server/src/wled_controller/static/js/core/command-palette.js b/server/src/wled_controller/static/js/core/command-palette.js index 5d19ced..21544a2 100644 --- a/server/src/wled_controller/static/js/core/command-palette.js +++ b/server/src/wled_controller/static/js/core/command-palette.js @@ -57,7 +57,7 @@ function _buildItems(results, states = {}) { _mapEntities(css, c => items.push({ name: c.name, detail: c.css_type || c.source_type, group: 'css', icon: getColorStripIcon(c.css_type || c.source_type), - nav: ['targets', 'led', 'led-css', 'data-css-id', c.id], + nav: ['streams', 'color_strip', 'color-strips', 'data-css-id', c.id], })); _mapEntities(automations, a => items.push({ diff --git a/server/src/wled_controller/static/js/features/streams.js b/server/src/wled_controller/static/js/features/streams.js index 3058fe2..85b3dbc 100644 --- a/server/src/wled_controller/static/js/features/streams.js +++ b/server/src/wled_controller/static/js/features/streams.js @@ -30,6 +30,7 @@ import { apiKey, streamsCache, ppTemplatesCache, captureTemplatesCache, audioSourcesCache, audioTemplatesCache, valueSourcesCache, syncClocksCache, filtersCache, + colorStripSourcesCache, } from '../core/state.js'; import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js'; import { t } from '../core/i18n.js'; @@ -41,8 +42,9 @@ import { TreeNav } from '../core/tree-nav.js'; import { updateSubTabHash } from './tabs.js'; import { createValueSourceCard } from './value-sources.js'; import { createSyncClockCard } from './sync-clocks.js'; +import { createColorStripCard } from './color-strips.js'; import { - getEngineIcon, getAudioEngineIcon, getPictureSourceIcon, getAudioSourceIcon, + getEngineIcon, getAudioEngineIcon, getPictureSourceIcon, getAudioSourceIcon, getColorStripIcon, ICON_TEMPLATE, ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_LINK_SOURCE, ICON_FPS, ICON_WEB, ICON_VALUE_SOURCE, ICON_CLOCK, ICON_AUDIO_LOOPBACK, ICON_AUDIO_INPUT, ICON_AUDIO_TEMPLATE, ICON_MONITOR, ICON_WRENCH, ICON_RADIO, @@ -69,6 +71,7 @@ const csAudioMulti = new CardSection('audio-multi', { titleKey: 'audio_source.gr const csAudioMono = new CardSection('audio-mono', { titleKey: 'audio_source.group.mono', gridClass: 'templates-grid', addCardOnclick: "showAudioSourceModal('mono')", keyAttr: 'data-id' }); const csStaticStreams = new CardSection('static-streams', { titleKey: 'streams.group.static_image', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('static_image')", keyAttr: 'data-stream-id' }); const csAudioTemplates = new CardSection('audio-templates', { titleKey: 'audio_template.title', gridClass: 'templates-grid', addCardOnclick: "showAddAudioTemplateModal()", keyAttr: 'data-audio-template-id' }); +const csColorStrips = new CardSection('color-strips', { titleKey: 'targets.section.color_strips', gridClass: 'templates-grid', addCardOnclick: "showCSSEditor()", keyAttr: 'data-css-id' }); const csValueSources = new CardSection('value-sources', { titleKey: 'value_source.group.title', gridClass: 'templates-grid', addCardOnclick: "showValueSourceModal()", keyAttr: 'data-id' }); const csSyncClocks = new CardSection('sync-clocks', { titleKey: 'sync_clock.group.title', gridClass: 'templates-grid', addCardOnclick: "showSyncClockModal()", keyAttr: 'data-id' }); @@ -1175,6 +1178,7 @@ export async function loadPictureSources() { valueSourcesCache.fetch(), syncClocksCache.fetch(), audioTemplatesCache.fetch(), + colorStripSourcesCache.fetch(), filtersCache.data.length === 0 ? filtersCache.fetch() : Promise.resolve(filtersCache.data), ]); renderPictureSourcesList(streams); @@ -1216,6 +1220,7 @@ const _streamSectionMap = { raw: [csRawStreams, csRawTemplates], static_image: [csStaticStreams], processed: [csProcStreams, csProcTemplates], + color_strip: [csColorStrips], audio: [csAudioMulti, csAudioMono, csAudioTemplates], value: [csValueSources], sync: [csSyncClocks], @@ -1367,11 +1372,18 @@ function renderPictureSourcesList(streams) { const multichannelSources = _cachedAudioSources.filter(s => s.source_type === 'multichannel'); const monoSources = _cachedAudioSources.filter(s => s.source_type === 'mono'); + // Color strip sources (maps needed for card rendering) + const colorStrips = colorStripSourcesCache.data; + const pictureSourceMap = {}; + streams.forEach(s => { pictureSourceMap[s.id] = s; }); + const audioSourceMap = {}; + _cachedAudioSources.forEach(s => { audioSourceMap[s.id] = s; }); const tabs = [ { key: 'raw', icon: getPictureSourceIcon('raw'), titleKey: 'streams.group.raw', count: rawStreams.length }, { key: 'static_image', icon: getPictureSourceIcon('static_image'), titleKey: 'streams.group.static_image', count: staticImageStreams.length }, { key: 'processed', icon: getPictureSourceIcon('processed'), titleKey: 'streams.group.processed', count: processedStreams.length }, + { key: 'color_strip', icon: getColorStripIcon('static'), titleKey: 'streams.group.color_strip', count: colorStrips.length }, { key: 'audio', icon: getAudioSourceIcon('multichannel'), titleKey: 'streams.group.audio', count: _cachedAudioSources.length }, { key: 'value', icon: ICON_VALUE_SOURCE, titleKey: 'streams.group.value', count: _cachedValueSources.length }, { key: 'sync', icon: ICON_CLOCK, titleKey: 'streams.group.sync', count: _cachedSyncClocks.length }, @@ -1387,6 +1399,10 @@ function renderPictureSourcesList(streams) { { key: 'processed', titleKey: 'streams.group.processed', icon: getPictureSourceIcon('processed'), count: processedStreams.length }, ] }, + { + key: 'color_strip', icon: getColorStripIcon('static'), titleKey: 'streams.group.color_strip', + count: colorStrips.length, + }, { key: 'audio', icon: getAudioSourceIcon('multichannel'), titleKey: 'streams.group.audio', count: _cachedAudioSources.length + _cachedAudioTemplates.length, @@ -1496,6 +1512,7 @@ function renderPictureSourcesList(streams) { const monoItems = csAudioMono.applySortOrder(monoSources.map(s => ({ key: s.id, html: renderAudioSourceCard(s) }))); const audioTemplateItems = csAudioTemplates.applySortOrder(_cachedAudioTemplates.map(t => ({ key: t.id, html: renderAudioTemplateCard(t) }))); const staticItems = csStaticStreams.applySortOrder(staticImageStreams.map(s => ({ key: s.id, html: renderStreamCard(s) }))); + const colorStripItems = csColorStrips.applySortOrder(colorStrips.map(s => ({ key: s.id, html: createColorStripCard(s, pictureSourceMap, audioSourceMap) }))); const valueItems = csValueSources.applySortOrder(_cachedValueSources.map(s => ({ key: s.id, html: createValueSourceCard(s) }))); const syncClockItems = csSyncClocks.applySortOrder(_cachedSyncClocks.map(s => ({ key: s.id, html: createSyncClockCard(s) }))); @@ -1505,6 +1522,7 @@ function renderPictureSourcesList(streams) { raw: rawStreams.length, static_image: staticImageStreams.length, processed: processedStreams.length, + color_strip: colorStrips.length, audio: _cachedAudioSources.length + _cachedAudioTemplates.length, value: _cachedValueSources.length, sync: _cachedSyncClocks.length, @@ -1513,6 +1531,7 @@ function renderPictureSourcesList(streams) { csRawTemplates.reconcile(rawTemplateItems); csProcStreams.reconcile(procStreamItems); csProcTemplates.reconcile(procTemplateItems); + csColorStrips.reconcile(colorStripItems); csAudioMulti.reconcile(multiItems); csAudioMono.reconcile(monoItems); csAudioTemplates.reconcile(audioTemplateItems); @@ -1525,6 +1544,7 @@ function renderPictureSourcesList(streams) { let panelContent = ''; if (tab.key === 'raw') panelContent = csRawStreams.render(rawStreamItems) + csRawTemplates.render(rawTemplateItems); else if (tab.key === 'processed') panelContent = csProcStreams.render(procStreamItems) + csProcTemplates.render(procTemplateItems); + else if (tab.key === 'color_strip') panelContent = csColorStrips.render(colorStripItems); else if (tab.key === 'audio') panelContent = csAudioMulti.render(multiItems) + csAudioMono.render(monoItems) + csAudioTemplates.render(audioTemplateItems); else if (tab.key === 'value') panelContent = csValueSources.render(valueItems); else if (tab.key === 'sync') panelContent = csSyncClocks.render(syncClockItems); @@ -1533,7 +1553,7 @@ function renderPictureSourcesList(streams) { }).join(''); container.innerHTML = panels; - CardSection.bindAll([csRawStreams, csRawTemplates, csProcStreams, csProcTemplates, csAudioMulti, csAudioMono, csAudioTemplates, csStaticStreams, csValueSources, csSyncClocks]); + CardSection.bindAll([csRawStreams, csRawTemplates, csProcStreams, csProcTemplates, csColorStrips, csAudioMulti, csAudioMono, csAudioTemplates, csStaticStreams, csValueSources, csSyncClocks]); // Render tree sidebar with expand/collapse buttons _streamsTree.setExtraHtml(``); @@ -1542,6 +1562,7 @@ function renderPictureSourcesList(streams) { 'raw-streams': 'raw', 'raw-templates': 'raw', 'static-streams': 'static_image', 'proc-streams': 'processed', 'proc-templates': 'processed', + 'color-strips': 'color_strip', 'audio-multi': 'audio', 'audio-mono': 'audio', 'audio-templates': 'audio', 'value-sources': 'value', 'sync-clocks': 'sync', diff --git a/server/src/wled_controller/static/js/features/targets.js b/server/src/wled_controller/static/js/features/targets.js index 90e4412..c498054 100644 --- a/server/src/wled_controller/static/js/features/targets.js +++ b/server/src/wled_controller/static/js/features/targets.js @@ -19,7 +19,6 @@ import { Modal } from '../core/modal.js'; import { createDeviceCard, attachDeviceListeners, fetchDeviceBrightness, enrichOpenrgbZoneBadges, _computeMaxFps, getZoneCountCache } from './devices.js'; import { _splitOpenrgbZone } from './device-discovery.js'; import { createKCTargetCard, patchKCTargetMetrics, connectKCWebSocket, disconnectKCWebSocket } from './kc-targets.js'; -import { createColorStripCard } from './color-strips.js'; import { getValueSourceIcon, getTargetTypeIcon, getDeviceTypeIcon, getColorStripIcon, ICON_CLONE, ICON_EDIT, ICON_START, ICON_STOP, @@ -41,7 +40,6 @@ import { updateSubTabHash, updateTabBadge } from './tabs.js'; // ── Card section instances ── const csDevices = new CardSection('led-devices', { titleKey: 'targets.section.devices', gridClass: 'devices-grid', addCardOnclick: "showAddDevice()", keyAttr: 'data-device-id' }); -const csColorStrips = new CardSection('led-css', { titleKey: 'targets.section.color_strips', gridClass: 'devices-grid', addCardOnclick: "showCSSEditor()", keyAttr: 'data-css-id' }); const csLedTargets = new CardSection('led-targets', { titleKey: 'targets.section.targets', gridClass: 'devices-grid', addCardOnclick: "showTargetEditor()", keyAttr: 'data-target-id', headerExtra: `` }); const csKCTargets = new CardSection('kc-targets', { titleKey: 'targets.section.key_colors', gridClass: 'devices-grid', addCardOnclick: "showKCEditor()", keyAttr: 'data-kc-target-id', headerExtra: `` }); const csPatternTemplates = new CardSection('kc-patterns', { titleKey: 'targets.section.pattern_templates', gridClass: 'templates-grid', addCardOnclick: "showPatternTemplateEditor()", keyAttr: 'data-pattern-template-id' }); @@ -569,7 +567,7 @@ export function expandAllTargetSections() { const activeSubTab = localStorage.getItem('activeTargetSubTab') || 'led'; const sections = activeSubTab === 'key_colors' ? [csKCTargets, csPatternTemplates] - : [csDevices, csColorStrips, csLedTargets]; + : [csDevices, csLedTargets]; CardSection.expandAll(sections); } @@ -577,7 +575,7 @@ export function collapseAllTargetSections() { const activeSubTab = localStorage.getItem('activeTargetSubTab') || 'led'; const sections = activeSubTab === 'key_colors' ? [csKCTargets, csPatternTemplates] - : [csDevices, csColorStrips, csLedTargets]; + : [csDevices, csLedTargets]; CardSection.collapseAll(sections); } @@ -605,7 +603,7 @@ export async function loadTargetsTab() { syncClocksCache.fetch().catch(() => []), ]); - let colorStripSourceMap = {}; + const colorStripSourceMap = {}; cssArr.forEach(s => { colorStripSourceMap[s.id] = s; }); let pictureSourceMap = {}; @@ -669,7 +667,6 @@ export async function loadTargetsTab() { key: 'led_group', icon: getTargetTypeIcon('led'), titleKey: 'targets.subtab.led', children: [ { key: 'led-devices', titleKey: 'targets.section.devices', icon: getDeviceTypeIcon('wled'), count: ledDevices.length, subTab: 'led', sectionKey: 'led-devices' }, - { key: 'led-css', titleKey: 'targets.section.color_strips', icon: getColorStripIcon('static'), count: Object.keys(colorStripSourceMap).length, subTab: 'led', sectionKey: 'led-css' }, { key: 'led-targets', titleKey: 'targets.section.targets', icon: getTargetTypeIcon('led'), count: ledTargets.length, subTab: 'led', sectionKey: 'led-targets' }, ] }, @@ -689,7 +686,6 @@ export async function loadTargetsTab() { // Build items arrays for each section (apply saved drag order) const deviceItems = csDevices.applySortOrder(ledDevices.map(d => ({ key: d.id, html: createDeviceCard(d) }))); - const cssItems = csColorStrips.applySortOrder(Object.values(colorStripSourceMap).map(s => ({ key: s.id, html: createColorStripCard(s, pictureSourceMap, audioSourceMap) }))); const ledTargetItems = csLedTargets.applySortOrder(ledTargets.map(t => ({ key: t.id, html: createTargetCard(t, deviceMap, colorStripSourceMap, valueSourceMap) }))); const kcTargetItems = csKCTargets.applySortOrder(kcTargets.map(t => ({ key: t.id, html: createKCTargetCard(t, pictureSourceMap, patternTemplateMap, valueSourceMap) }))); const patternItems = csPatternTemplates.applySortOrder(patternTemplates.map(pt => ({ key: pt.id, html: createPatternTemplateCard(pt) }))); @@ -701,13 +697,11 @@ export async function loadTargetsTab() { // ── Incremental update: reconcile cards in-place ── _targetsTree.updateCounts({ 'led-devices': ledDevices.length, - 'led-css': Object.keys(colorStripSourceMap).length, 'led-targets': ledTargets.length, 'kc-targets': kcTargets.length, 'kc-patterns': patternTemplates.length, }); csDevices.reconcile(deviceItems); - csColorStrips.reconcile(cssItems); const ledResult = csLedTargets.reconcile(ledTargetItems); const kcResult = csKCTargets.reconcile(kcTargetItems); csPatternTemplates.reconcile(patternItems); @@ -727,7 +721,6 @@ export async function loadTargetsTab() { const ledPanel = `