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 = `
${csDevices.render(deviceItems)} - ${csColorStrips.render(cssItems)} ${csLedTargets.render(ledTargetItems)}
`; const kcPanel = ` @@ -736,7 +729,7 @@ export async function loadTargetsTab() { ${csPatternTemplates.render(patternItems)} `; container.innerHTML = ledPanel + kcPanel; - CardSection.bindAll([csDevices, csColorStrips, csLedTargets, csKCTargets, csPatternTemplates]); + CardSection.bindAll([csDevices, csLedTargets, csKCTargets, csPatternTemplates]); // Render tree sidebar with expand/collapse buttons _targetsTree.setExtraHtml(``); @@ -1011,7 +1004,7 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo ${ICON_LED} ${escapeHtml(deviceName)} ${ICON_FPS} ${target.fps || 30} ${_protocolBadge(device, target)} - ${ICON_FILM} ${cssSummary} + ${ICON_FILM} ${cssSummary} ${bvs ? `${getValueSourceIcon(bvs.source_type)} ${escapeHtml(bvs.name)}` : ''} ${target.min_brightness_threshold > 0 ? `${ICON_SUN_DIM} <${target.min_brightness_threshold} → off` : ''} diff --git a/server/src/wled_controller/static/js/features/tutorials.js b/server/src/wled_controller/static/js/features/tutorials.js index 298568d..b2e0d03 100644 --- a/server/src/wled_controller/static/js/features/tutorials.js +++ b/server/src/wled_controller/static/js/features/tutorials.js @@ -46,7 +46,6 @@ const dashboardTutorialSteps = [ const targetsTutorialSteps = [ { selector: '[data-tree-group="led_group"]', textKey: 'tour.tgt.led_tab', position: 'right' }, { selector: '[data-card-section="led-devices"]', textKey: 'tour.tgt.devices', position: 'bottom' }, - { selector: '[data-card-section="led-css"]', textKey: 'tour.tgt.css', position: 'bottom' }, { selector: '[data-card-section="led-targets"]', textKey: 'tour.tgt.targets', position: 'bottom' }, { selector: '[data-tree-group="kc_group"]', textKey: 'tour.tgt.kc_tab', position: 'right' } ]; @@ -56,6 +55,7 @@ const sourcesTourSteps = [ { selector: '[data-card-section="raw-templates"]', textKey: 'tour.src.templates', position: 'bottom' }, { selector: '#streams-tree-nav [data-tree-leaf="static_image"]', textKey: 'tour.src.static', position: 'right' }, { selector: '#streams-tree-nav [data-tree-leaf="processed"]', textKey: 'tour.src.processed', position: 'right' }, + { selector: '#streams-tree-nav [data-tree-leaf="color_strip"]', textKey: 'tour.src.color_strip', position: 'right' }, { selector: '#streams-tree-nav [data-tree-leaf="audio"]', textKey: 'tour.src.audio', position: 'right' }, { selector: '#streams-tree-nav [data-tree-leaf="value"]', textKey: 'tour.src.value', position: 'right' }, { selector: '#streams-tree-nav [data-tree-leaf="sync"]', textKey: 'tour.src.sync', position: 'right' } diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index ec19d3b..76da6d7 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -355,6 +355,7 @@ "tour.src.templates": "Capture Templates — reusable capture configurations (resolution, FPS, crop).", "tour.src.static": "Static Image — test your setup with image files instead of live capture.", "tour.src.processed": "Processed — apply post-processing effects like blur, brightness, or color correction.", + "tour.src.color_strip": "Color Strips — define how screen regions map to LED segments.", "tour.src.audio": "Audio — analyze microphone or system audio for reactive LED effects.", "tour.src.value": "Value — numeric data sources used as conditions in automations.", "tour.src.sync": "Sync Clocks — shared timers that synchronize animations across multiple sources.", @@ -416,6 +417,7 @@ "streams.description": "Sources define the capture pipeline. A raw source captures from a display using a capture template. A processed source applies postprocessing to another source. Assign sources to devices.", "streams.group.raw": "Screen Capture", "streams.group.processed": "Processed", + "streams.group.color_strip": "Color Strips", "streams.group.audio": "Audio", "streams.section.streams": "Sources", "streams.add": "Add Source", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index bf736a8..1c9ada9 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -304,6 +304,7 @@ "tour.src.templates": "Шаблоны захвата — переиспользуемые конфигурации (разрешение, FPS, обрезка).", "tour.src.static": "Статичные изображения — тестируйте настройку с файлами изображений.", "tour.src.processed": "Обработка — применяйте эффекты: размытие, яркость, цветокоррекция.", + "tour.src.color_strip": "Цветовые полосы — определяют, как области экрана сопоставляются с LED-сегментами.", "tour.src.audio": "Аудио — анализ микрофона или системного звука для реактивных LED-эффектов.", "tour.src.value": "Значения — числовые источники данных для условий автоматизаций.", "tour.src.sync": "Синхро-часы — общие таймеры для синхронизации анимаций между несколькими источниками.", @@ -365,6 +366,7 @@ "streams.description": "Источники определяют конвейер захвата. Сырой источник захватывает экран с помощью шаблона захвата. Обработанный источник применяет постобработку к другому источнику. Назначайте источники устройствам.", "streams.group.raw": "Захват Экрана", "streams.group.processed": "Обработанные", + "streams.group.color_strip": "Цветовые Полосы", "streams.group.audio": "Аудио", "streams.section.streams": "Источники", "streams.add": "Добавить Источник", diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index d7cc613..2ce76c4 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -304,6 +304,7 @@ "tour.src.templates": "捕获模板 — 可复用的捕获配置(分辨率、FPS、裁剪)。", "tour.src.static": "静态图片 — 使用图片文件测试您的设置。", "tour.src.processed": "处理 — 应用后处理效果,如模糊、亮度或色彩校正。", + "tour.src.color_strip": "色带 — 定义屏幕区域如何映射到 LED 段。", "tour.src.audio": "音频 — 分析麦克风或系统音频以实现响应式 LED 效果。", "tour.src.value": "数值 — 用于自动化条件的数字数据源。", "tour.src.sync": "同步时钟 — 在多个源之间同步动画的共享定时器。", @@ -365,6 +366,7 @@ "streams.description": "源定义采集管线。原始源使用采集模板从显示器采集。处理源对另一个源应用后处理。将源分配给设备。", "streams.group.raw": "屏幕采集", "streams.group.processed": "已处理", + "streams.group.color_strip": "色带源", "streams.group.audio": "音频", "streams.section.streams": "源", "streams.add": "添加源",