From 6c7b7ea7d7326cbc5b1b7c2e911a99c8c7f06468 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sun, 15 Mar 2026 12:32:13 +0300 Subject: [PATCH] Separate tree nodes into independent panels, remove graph local search, UI improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split Sources tab: raw/raw_templates, processed/proc_templates each get own panel - Split Targets tab: led-devices, led-targets, kc-targets, kc-patterns each get own panel - Remove graph local search — search button and / key open global command palette - Add graphNavigateToNode for command palette → graph node navigation - Add tree group expand/collapse animation (max-height + opacity transition) - Make tree group headers visually distinct (smaller, uppercase, left border on children) - Make CardSection collapse opt-in via collapsible flag (disabled by default) - Move filter textbox next to section title (remove margin-left: auto) - Fix notification bell button vertical centering in test preview - Fix clipboard copy on non-HTTPS with execCommand fallback - Add overlay toggle button on picture-based CSS cards - Add CSPT to graph add-entity picker and global search - Update all cross-link navigation paths for new panel keys - Add i18n keys for new tree groups and search groups (en/ru/zh) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../wled_controller/static/css/streams.css | 7 +- .../wled_controller/static/css/tree-nav.css | 32 ++++-- server/src/wled_controller/static/js/app.js | 4 +- .../static/js/core/card-sections.js | 17 +-- .../static/js/core/command-palette.js | 12 +- .../static/js/features/graph-editor.js | 106 +----------------- .../static/js/features/kc-targets.js | 2 +- .../static/js/features/streams.js | 38 +++++-- .../static/js/features/targets.js | 70 +++++------- .../wled_controller/static/locales/en.json | 8 +- .../wled_controller/static/locales/ru.json | 8 +- .../wled_controller/static/locales/zh.json | 8 +- 12 files changed, 127 insertions(+), 185 deletions(-) diff --git a/server/src/wled_controller/static/css/streams.css b/server/src/wled_controller/static/css/streams.css index 3deda18..ef658a5 100644 --- a/server/src/wled_controller/static/css/streams.css +++ b/server/src/wled_controller/static/css/streams.css @@ -732,11 +732,15 @@ body.pp-filter-dragging .pp-filter-drag-handle { display: flex; align-items: center; gap: 8px; - cursor: pointer; + cursor: default; user-select: none; padding: 8px 0; } +.cs-header:has(.cs-chevron) { + cursor: pointer; +} + /* Prevent sticky header from clipping cards that lift on hover */ [data-cs-content] { padding-top: 4px; @@ -783,7 +787,6 @@ body.pp-filter-dragging .pp-filter-drag-handle { .cs-filter-wrap { position: relative; - margin-left: auto; width: 180px; max-width: 40%; flex-shrink: 0; diff --git a/server/src/wled_controller/static/css/tree-nav.css b/server/src/wled_controller/static/css/tree-nav.css index f18b170..09e3d3e 100644 --- a/server/src/wled_controller/static/css/tree-nav.css +++ b/server/src/wled_controller/static/css/tree-nav.css @@ -30,6 +30,10 @@ margin-bottom: 2px; } +.tree-group:first-child > .tree-group-header { + margin-top: 0; +} + .tree-group-header { display: flex; align-items: center; @@ -37,13 +41,14 @@ padding: 6px 10px; cursor: pointer; user-select: none; - font-size: 0.78rem; - font-weight: 600; - color: var(--text-secondary); + font-size: 0.72rem; + font-weight: 700; + color: var(--text-muted); border-radius: 6px; transition: color 0.15s, background 0.15s; text-transform: uppercase; - letter-spacing: 0.03em; + letter-spacing: 0.05em; + margin-top: 6px; } .tree-group-header:hover { @@ -98,21 +103,28 @@ .tree-children { overflow: hidden; + margin-left: 14px; + border-left: 1px solid var(--border-color); + padding-left: 0; + max-height: 500px; + opacity: 1; + transition: max-height 0.25s ease, opacity 0.2s ease; } .tree-children.collapsed { - display: none; + max-height: 0; + opacity: 0; } .tree-leaf { display: flex; align-items: center; gap: 6px; - padding: 5px 10px 5px 26px; + padding: 5px 10px 5px 12px; cursor: pointer; font-size: 0.8rem; color: var(--text-secondary); - border-radius: 6px; + border-radius: 0 6px 6px 0; margin: 1px 0; transition: color 0.15s, background 0.15s; } @@ -243,6 +255,12 @@ display: flex; flex-wrap: wrap; gap: 2px; + margin-left: 0; + border-left: none; + padding-left: 0; + max-height: none; + opacity: 1; + transition: none; } .tree-children.collapsed { diff --git a/server/src/wled_controller/static/js/app.js b/server/src/wled_controller/static/js/app.js index a158faf..2a92844 100644 --- a/server/src/wled_controller/static/js/app.js +++ b/server/src/wled_controller/static/js/app.js @@ -163,7 +163,7 @@ import { // Layer 5.5: graph editor import { - loadGraphEditor, openGraphSearch, closeGraphSearch, + loadGraphEditor, toggleGraphLegend, toggleGraphMinimap, toggleGraphFilter, graphFitAll, graphZoomIn, graphZoomOut, graphRelayout, graphToggleFullscreen, graphAddEntity, @@ -481,8 +481,6 @@ Object.assign(window, { // graph editor loadGraphEditor, - openGraphSearch, - closeGraphSearch, toggleGraphLegend, toggleGraphMinimap, toggleGraphFilter, diff --git a/server/src/wled_controller/static/js/core/card-sections.js b/server/src/wled_controller/static/js/core/card-sections.js index 00afb59..b5e59cb 100644 --- a/server/src/wled_controller/static/js/core/card-sections.js +++ b/server/src/wled_controller/static/js/core/card-sections.js @@ -45,13 +45,14 @@ export class CardSection { * @param {string} [opts.keyAttr] data attribute that uniquely identifies cards (e.g. 'data-device-id') * @param {string} [opts.headerExtra] Extra HTML injected between count badge and filter (e.g. action buttons) */ - constructor(sectionKey, { titleKey, gridClass, addCardOnclick, keyAttr, headerExtra }) { + constructor(sectionKey, { titleKey, gridClass, addCardOnclick, keyAttr, headerExtra, collapsible }) { this.sectionKey = sectionKey; this.titleKey = titleKey; this.gridClass = gridClass; this.addCardOnclick = addCardOnclick || ''; this.keyAttr = keyAttr || ''; this.headerExtra = headerExtra || ''; + this.collapsible = !!collapsible; this._filterValue = ''; this._lastItems = null; this._dragState = null; @@ -73,7 +74,7 @@ export class CardSection { const count = items.length; const cardsHtml = items.map(i => i.html).join(''); - const isCollapsed = !!_getCollapsedMap()[this.sectionKey]; + const isCollapsed = this.collapsible && !!_getCollapsedMap()[this.sectionKey]; const chevronStyle = isCollapsed ? '' : ' style="transform:rotate(90deg)"'; const contentDisplay = isCollapsed ? ' style="display:none"' : ''; const collapsedClass = isCollapsed ? ' cs-collapsed' : ''; @@ -85,7 +86,7 @@ export class CardSection { return `
- + ${this.collapsible ? `` : ''} ${t(this.titleKey)} ${count} ${this.headerExtra ? `${this.headerExtra}` : ''} @@ -109,10 +110,12 @@ export class CardSection { const filterInput = document.querySelector(`[data-cs-filter="${this.sectionKey}"]`); if (!header || !content) return; - header.addEventListener('mousedown', (e) => { - if (e.target.closest('.cs-filter-wrap') || e.target.closest('.cs-header-extra')) return; - this._toggleCollapse(header, content); - }); + if (this.collapsible) { + header.addEventListener('mousedown', (e) => { + if (e.target.closest('.cs-filter-wrap') || e.target.closest('.cs-header-extra')) return; + this._toggleCollapse(header, content); + }); + } if (filterInput) { const resetBtn = document.querySelector(`[data-cs-filter-reset="${this.sectionKey}"]`); 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 e2f633a..ffe7731 100644 --- a/server/src/wled_controller/static/js/core/command-palette.js +++ b/server/src/wled_controller/static/js/core/command-palette.js @@ -38,7 +38,7 @@ function _buildItems(results, states = {}) { _mapEntities(devices, d => items.push({ name: d.name, detail: d.device_type, group: 'devices', icon: ICON_DEVICE, - nav: ['targets', 'led', 'led-devices', 'data-device-id', d.id], + nav: ['targets', 'led-devices', 'led-devices', 'data-device-id', d.id], })); _mapEntities(targets, tgt => { @@ -46,12 +46,12 @@ function _buildItems(results, states = {}) { if (tgt.target_type === 'key_colors') { items.push({ name: tgt.name, detail: 'key_colors', group: 'kc_targets', icon: getTargetTypeIcon('key_colors'), - nav: ['targets', 'key_colors', 'kc-targets', 'data-kc-target-id', tgt.id], running, + nav: ['targets', 'kc-targets', 'kc-targets', 'data-kc-target-id', tgt.id], running, }); } else { items.push({ name: tgt.name, detail: tgt.target_type, group: 'targets', icon: ICON_TARGET, - nav: ['targets', 'led', 'led-targets', 'data-target-id', tgt.id], running, + nav: ['targets', 'led-targets', 'led-targets', 'data-target-id', tgt.id], running, }); } }); @@ -68,17 +68,17 @@ function _buildItems(results, states = {}) { _mapEntities(capTempl, ct => items.push({ name: ct.name, detail: ct.engine_type, group: 'capture_templates', icon: ICON_CAPTURE_TEMPLATE, - nav: ['streams', 'raw', 'raw-templates', 'data-template-id', ct.id], + nav: ['streams', 'raw_templates', 'raw-templates', 'data-template-id', ct.id], })); _mapEntities(ppTempl, pp => items.push({ name: pp.name, detail: '', group: 'pp_templates', icon: ICON_PP_TEMPLATE, - nav: ['streams', 'processed', 'proc-templates', 'data-pp-template-id', pp.id], + nav: ['streams', 'proc_templates', 'proc-templates', 'data-pp-template-id', pp.id], })); _mapEntities(patTempl, pt => items.push({ name: pt.name, detail: '', group: 'pattern_templates', icon: ICON_PATTERN_TEMPLATE, - nav: ['targets', 'key_colors', 'kc-patterns', 'data-pattern-template-id', pt.id], + nav: ['targets', 'kc-patterns', 'kc-patterns', 'data-pattern-template-id', pt.id], })); _mapEntities(audioSrc, a => { diff --git a/server/src/wled_controller/static/js/features/graph-editor.js b/server/src/wled_controller/static/js/features/graph-editor.js index 5630c15..e73af9c 100644 --- a/server/src/wled_controller/static/js/features/graph-editor.js +++ b/server/src/wled_controller/static/js/features/graph-editor.js @@ -28,9 +28,6 @@ let _selectedIds = new Set(); let _initialized = false; let _legendVisible = (() => { try { return localStorage.getItem('graph_legend_visible') === '1'; } catch { return false; } })(); let _minimapVisible = true; -let _searchVisible = false; -let _searchIndex = -1; -let _searchItems = []; let _loading = false; let _filterVisible = false; let _filterQuery = ''; // current active filter text @@ -209,29 +206,6 @@ export async function loadGraphEditor() { } } -export function openGraphSearch() { - if (!_nodeMap) return; - const panel = document.querySelector('.graph-search'); - if (!panel) return; - - _searchItems = []; - for (const node of _nodeMap.values()) _searchItems.push(node); - - _searchIndex = -1; - _searchVisible = true; - panel.classList.add('visible'); - const input = panel.querySelector('.graph-search-input'); - input.value = ''; - input.focus(); - _renderSearchResults(''); -} - -export function closeGraphSearch() { - _searchVisible = false; - const panel = document.querySelector('.graph-search'); - if (panel) panel.classList.remove('visible'); -} - export function toggleGraphLegend() { _legendVisible = !_legendVisible; try { localStorage.setItem('graph_legend_visible', _legendVisible ? '1' : '0'); } catch {} @@ -587,12 +561,6 @@ function _renderGraph(container) { _onEdgeContextMenu(edgePath, e, container); }); - const searchInput = container.querySelector('.graph-search-input'); - if (searchInput) { - searchInput.addEventListener('input', (e) => _renderSearchResults(e.target.value)); - searchInput.addEventListener('keydown', _onSearchKeydown); - } - const filterInput = container.querySelector('.graph-filter-input'); if (filterInput) { filterInput.addEventListener('input', (e) => _applyFilter(e.target.value)); @@ -714,7 +682,7 @@ function _graphHTML() { -
- -
@@ -997,60 +960,6 @@ function _initToolbarDrag(tbEl) { _makeDraggable(tbEl, handle, { loadFn: _loadToolbarPos, saveFn: _saveToolbarPos }); } -/* ── Search ── */ - -function _renderSearchResults(query) { - const results = document.querySelector('.graph-search-results'); - if (!results) return; - - const q = query.toLowerCase().trim(); - const filtered = q - ? _searchItems.filter(n => n.name.toLowerCase().includes(q) || n.kind.includes(q) || (n.subtype || '').includes(q)) - : _searchItems.slice(0, 20); - - _searchIndex = filtered.length > 0 ? 0 : -1; - - results.innerHTML = filtered.map((n, i) => { - const color = ENTITY_COLORS[n.kind] || '#666'; - return `
- - ${_escHtml(n.name)} - ${n.kind.replace(/_/g, ' ')} -
`; - }).join(''); - - results.querySelectorAll('.graph-search-item').forEach(item => { - item.addEventListener('click', () => { - _navigateToNode(item.getAttribute('data-id')); - closeGraphSearch(); - }); - }); -} - -function _onSearchKeydown(e) { - const results = document.querySelectorAll('.graph-search-item'); - if (e.key === 'ArrowDown') { e.preventDefault(); _searchIndex = Math.min(_searchIndex + 1, results.length - 1); _updateSearchActive(results); } - else if (e.key === 'ArrowUp') { e.preventDefault(); _searchIndex = Math.max(_searchIndex - 1, 0); _updateSearchActive(results); } - else if (e.key === 'Enter') { e.preventDefault(); if (results[_searchIndex]) { _navigateToNode(results[_searchIndex].getAttribute('data-id')); closeGraphSearch(); } } - else if (e.key === 'Escape') { closeGraphSearch(); } -} - -function _updateSearchActive(items) { - items.forEach((el, i) => el.classList.toggle('active', i === _searchIndex)); - if (items[_searchIndex]) items[_searchIndex].scrollIntoView({ block: 'nearest' }); -} - -function _navigateToNode(nodeId) { - const node = _nodeMap?.get(nodeId); - if (!node || !_canvas) return; - _canvas.panTo(node.x + node.width / 2, node.y + node.height / 2, true); - - const nodeGroup = document.querySelector('.graph-nodes'); - if (nodeGroup) { highlightNode(nodeGroup, nodeId); setTimeout(() => highlightNode(nodeGroup, null), 3000); } - - const edgeGroup = document.querySelector('.graph-edges'); - if (edgeGroup && _edges) { highlightChain(edgeGroup, nodeId, _edges); setTimeout(() => clearEdgeHighlights(edgeGroup), 5000); } -} /* ── Node callbacks ── */ @@ -1213,11 +1122,10 @@ function _onKeydown(e) { // Skip when typing in search input (except Escape/F11) const inInput = e.target.matches('input, textarea, select'); - if (e.key === '/' && !_searchVisible && !inInput) { e.preventDefault(); openGraphSearch(); } + if (e.key === '/' && !inInput) { e.preventDefault(); window.openCommandPalette?.(); } if ((e.key === 'f' || e.key === 'F') && !inInput && !e.ctrlKey && !e.metaKey) { e.preventDefault(); toggleGraphFilter(); } if (e.key === 'Escape') { if (_filterVisible) { toggleGraphFilter(); } - else if (_searchVisible) { closeGraphSearch(); } else { const ng = document.querySelector('.graph-nodes'); const eg = document.querySelector('.graph-edges'); @@ -1249,7 +1157,7 @@ function _onKeydown(e) { graphAddEntity(); } // Arrow keys / WASD → spatial navigation between nodes - if (_selectedIds.size <= 1 && !_searchVisible && !inInput) { + if (_selectedIds.size <= 1 && !inInput) { const dir = _arrowDir(e); if (dir) { e.preventDefault(); @@ -1649,14 +1557,6 @@ function _calcBounds(nodeMap) { return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; } -/* ── Helpers ── */ - -function _escHtml(s) { - const d = document.createElement('div'); - d.textContent = s; - return d.innerHTML; -} - /* ── Port drag (connect/reconnect) ── */ const SVG_NS = 'http://www.w3.org/2000/svg'; diff --git a/server/src/wled_controller/static/js/features/kc-targets.js b/server/src/wled_controller/static/js/features/kc-targets.js index 6771fa0..7ec4b66 100644 --- a/server/src/wled_controller/static/js/features/kc-targets.js +++ b/server/src/wled_controller/static/js/features/kc-targets.js @@ -228,7 +228,7 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueS
${ICON_LINK_SOURCE} ${escapeHtml(sourceName)} - ${ICON_PATTERN_TEMPLATE} ${escapeHtml(patternName)} + ${ICON_PATTERN_TEMPLATE} ${escapeHtml(patternName)} ▭ ${rectCount} rect${rectCount !== 1 ? 's' : ''} ${ICON_FPS} ${kcSettings.fps ?? 10} ${bvs ? `${getValueSourceIcon(bvs.source_type)} ${escapeHtml(bvs.name)}` : ''} diff --git a/server/src/wled_controller/static/js/features/streams.js b/server/src/wled_controller/static/js/features/streams.js index 621cd50..0c8abb4 100644 --- a/server/src/wled_controller/static/js/features/streams.js +++ b/server/src/wled_controller/static/js/features/streams.js @@ -1246,9 +1246,11 @@ export function switchStreamTab(tabKey) { } const _streamSectionMap = { - raw: [csRawStreams, csRawTemplates], + raw: [csRawStreams], + raw_templates: [csRawTemplates], static_image: [csStaticStreams], - processed: [csProcStreams, csProcTemplates], + processed: [csProcStreams], + proc_templates: [csProcTemplates], css_processing: [csCSPTemplates], color_strip: [csColorStrips], audio: [csAudioMulti, csAudioMono, csAudioTemplates], @@ -1283,7 +1285,7 @@ function renderPictureSourcesList(streams) { detailsHtml = `
${ICON_MONITOR} ${stream.display_index ?? 0} ${ICON_FPS} ${stream.target_fps ?? 30} - ${capTmplName ? `${ICON_TEMPLATE} ${capTmplName}` : ''} + ${capTmplName ? `${ICON_TEMPLATE} ${capTmplName}` : ''}
`; } else if (stream.stream_type === 'processed') { const sourceStream = _cachedStreams.find(s => s.id === stream.source_stream_id); @@ -1297,7 +1299,7 @@ function renderPictureSourcesList(streams) { } detailsHtml = `
${ICON_LINK_SOURCE} ${sourceName} - ${ppTmplName ? `${ICON_TEMPLATE} ${ppTmplName}` : ''} + ${ppTmplName ? `${ICON_TEMPLATE} ${ppTmplName}` : ''}
`; } else if (stream.stream_type === 'static_image') { const src = stream.image_source || ''; @@ -1440,8 +1442,10 @@ function renderPictureSourcesList(streams) { const tabs = [ { key: 'raw', icon: getPictureSourceIcon('raw'), titleKey: 'streams.group.raw', count: rawStreams.length }, + { key: 'raw_templates', icon: ICON_CAPTURE_TEMPLATE, titleKey: 'streams.group.raw_templates', count: _cachedCaptureTemplates.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: 'proc_templates', icon: ICON_PP_TEMPLATE, titleKey: 'streams.group.proc_templates', count: _cachedPPTemplates.length }, { key: 'css_processing', icon: ICON_CSPT, titleKey: 'streams.group.css_processing', count: csptTemplates.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 }, @@ -1452,11 +1456,21 @@ function renderPictureSourcesList(streams) { // Build tree navigation structure const treeGroups = [ { - key: 'picture_group', icon: getPictureSourceIcon('raw'), titleKey: 'tree.group.picture', + key: 'capture_group', icon: getPictureSourceIcon('raw'), titleKey: 'tree.group.capture', children: [ { key: 'raw', titleKey: 'streams.group.raw', icon: getPictureSourceIcon('raw'), count: rawStreams.length }, - { key: 'static_image', titleKey: 'streams.group.static_image', icon: getPictureSourceIcon('static_image'), count: staticImageStreams.length }, + { key: 'raw_templates', titleKey: 'streams.group.raw_templates', icon: ICON_CAPTURE_TEMPLATE, count: _cachedCaptureTemplates.length }, + ] + }, + { + key: 'static_image', icon: getPictureSourceIcon('static_image'), titleKey: 'streams.group.static_image', + count: staticImageStreams.length, + }, + { + key: 'processing_group', icon: getPictureSourceIcon('processed'), titleKey: 'tree.group.processing', + children: [ { key: 'processed', titleKey: 'streams.group.processed', icon: getPictureSourceIcon('processed'), count: processedStreams.length }, + { key: 'proc_templates', titleKey: 'streams.group.proc_templates', icon: ICON_PP_TEMPLATE, count: _cachedPPTemplates.length }, ] }, { @@ -1584,8 +1598,10 @@ function renderPictureSourcesList(streams) { // Incremental update: reconcile cards in-place _streamsTree.updateCounts({ raw: rawStreams.length, + raw_templates: _cachedCaptureTemplates.length, static_image: staticImageStreams.length, processed: processedStreams.length, + proc_templates: _cachedPPTemplates.length, css_processing: csptTemplates.length, color_strip: colorStrips.length, audio: _cachedAudioSources.length + _cachedAudioTemplates.length, @@ -1608,8 +1624,10 @@ function renderPictureSourcesList(streams) { // First render: build full HTML const panels = tabs.map(tab => { 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); + if (tab.key === 'raw') panelContent = csRawStreams.render(rawStreamItems); + else if (tab.key === 'raw_templates') panelContent = csRawTemplates.render(rawTemplateItems); + else if (tab.key === 'processed') panelContent = csProcStreams.render(procStreamItems); + else if (tab.key === 'proc_templates') panelContent = csProcTemplates.render(procTemplateItems); else if (tab.key === 'css_processing') panelContent = csCSPTemplates.render(csptItems); 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); @@ -1626,9 +1644,9 @@ function renderPictureSourcesList(streams) { _streamsTree.setExtraHtml(``); _streamsTree.update(treeGroups, activeTab); _streamsTree.observeSections('streams-list', { - 'raw-streams': 'raw', 'raw-templates': 'raw', + 'raw-streams': 'raw', 'raw-templates': 'raw_templates', 'static-streams': 'static_image', - 'proc-streams': 'processed', 'proc-templates': 'processed', + 'proc-streams': 'processed', 'proc-templates': 'proc_templates', 'css-proc-templates': 'css_processing', 'color-strips': 'color_strip', 'audio-multi': 'audio', 'audio-mono': 'audio', 'audio-templates': 'audio', diff --git a/server/src/wled_controller/static/js/features/targets.js b/server/src/wled_controller/static/js/features/targets.js index c498054..900d5f1 100644 --- a/server/src/wled_controller/static/js/features/targets.js +++ b/server/src/wled_controller/static/js/features/targets.js @@ -535,18 +535,10 @@ export async function saveTargetEditor() { let _treeTriggered = false; const _targetsTree = new TreeNav('targets-tree-nav', { - onSelect: (key, leaf) => { - const subTab = leaf?.subTab || key; + onSelect: (key) => { _treeTriggered = true; - switchTargetSubTab(subTab); + switchTargetSubTab(key); _treeTriggered = false; - // Scroll to specific section - if (leaf?.sectionKey) { - requestAnimationFrame(() => { - const section = document.querySelector(`[data-card-section="${leaf.sectionKey}"]`); - if (section) section.scrollIntoView({ behavior: 'smooth', block: 'start' }); - }); - } } }); @@ -558,25 +550,25 @@ export function switchTargetSubTab(tabKey) { updateSubTabHash('targets', tabKey); // Update tree active state (unless the tree triggered this switch) if (!_treeTriggered) { - const leafKey = _targetsTree.getLeafForSubTab(tabKey); - if (leafKey) _targetsTree.setActive(leafKey); + _targetsTree.setActive(tabKey); } } +const _targetSectionMap = { + 'led-devices': [csDevices], + 'led-targets': [csLedTargets], + 'kc-targets': [csKCTargets], + 'kc-patterns': [csPatternTemplates], +}; + export function expandAllTargetSections() { - const activeSubTab = localStorage.getItem('activeTargetSubTab') || 'led'; - const sections = activeSubTab === 'key_colors' - ? [csKCTargets, csPatternTemplates] - : [csDevices, csLedTargets]; - CardSection.expandAll(sections); + const activeSubTab = localStorage.getItem('activeTargetSubTab') || 'led-devices'; + CardSection.expandAll(_targetSectionMap[activeSubTab] || []); } export function collapseAllTargetSections() { - const activeSubTab = localStorage.getItem('activeTargetSubTab') || 'led'; - const sections = activeSubTab === 'key_colors' - ? [csKCTargets, csPatternTemplates] - : [csDevices, csLedTargets]; - CardSection.collapseAll(sections); + const activeSubTab = localStorage.getItem('activeTargetSubTab') || 'led-devices'; + CardSection.collapseAll(_targetSectionMap[activeSubTab] || []); } let _loadTargetsLock = false; @@ -666,20 +658,22 @@ 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-targets', titleKey: 'targets.section.targets', icon: getTargetTypeIcon('led'), count: ledTargets.length, subTab: 'led', sectionKey: 'led-targets' }, + { key: 'led-devices', titleKey: 'targets.section.devices', icon: getDeviceTypeIcon('wled'), count: ledDevices.length }, + { key: 'led-targets', titleKey: 'targets.section.targets', icon: getTargetTypeIcon('led'), count: ledTargets.length }, ] }, { key: 'kc_group', icon: getTargetTypeIcon('key_colors'), titleKey: 'targets.subtab.key_colors', children: [ - { key: 'kc-targets', titleKey: 'targets.section.key_colors', icon: getTargetTypeIcon('key_colors'), count: kcTargets.length, subTab: 'key_colors', sectionKey: 'kc-targets' }, - { key: 'kc-patterns', titleKey: 'targets.section.pattern_templates', icon: ICON_TEMPLATE, count: patternTemplates.length, subTab: 'key_colors', sectionKey: 'kc-patterns' }, + { key: 'kc-targets', titleKey: 'targets.section.key_colors', icon: getTargetTypeIcon('key_colors'), count: kcTargets.length }, + { key: 'kc-patterns', titleKey: 'targets.section.pattern_templates', icon: ICON_TEMPLATE, count: patternTemplates.length }, ] } ]; - // Determine which tree leaf is active - const activeLeaf = activeSubTab === 'key_colors' ? 'kc-targets' : 'led-devices'; + // Determine which tree leaf is active — migrate old values + const validLeaves = ['led-devices', 'led-targets', 'kc-targets', 'kc-patterns']; + const activeLeaf = validLeaves.includes(activeSubTab) ? activeSubTab + : activeSubTab === 'key_colors' ? 'kc-targets' : 'led-devices'; // Use window.createPatternTemplateCard to avoid circular import const createPatternTemplateCard = window.createPatternTemplateCard || (() => ''); @@ -718,17 +712,13 @@ export async function loadTargetsTab() { } } else { // ── First render: build full HTML ── - const ledPanel = ` -
- ${csDevices.render(deviceItems)} - ${csLedTargets.render(ledTargetItems)} -
`; - const kcPanel = ` -
- ${csKCTargets.render(kcTargetItems)} - ${csPatternTemplates.render(patternItems)} -
`; - container.innerHTML = ledPanel + kcPanel; + const panels = [ + { key: 'led-devices', html: csDevices.render(deviceItems) }, + { key: 'led-targets', html: csLedTargets.render(ledTargetItems) }, + { key: 'kc-targets', html: csKCTargets.render(kcTargetItems) }, + { key: 'kc-patterns', html: csPatternTemplates.render(patternItems) }, + ].map(p => `
${p.html}
`).join(''); + container.innerHTML = panels; CardSection.bindAll([csDevices, csLedTargets, csKCTargets, csPatternTemplates]); // Render tree sidebar with expand/collapse buttons @@ -1001,7 +991,7 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
- ${ICON_LED} ${escapeHtml(deviceName)} + ${ICON_LED} ${escapeHtml(deviceName)} ${ICON_FPS} ${target.fps || 30} ${_protocolBadge(device, target)} ${ICON_FILM} ${cssSummary} diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index b834bef..52cd5a3 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -417,8 +417,10 @@ "section.collapse_all": "Collapse all sections", "streams.title": "Sources", "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.raw": "Sources", + "streams.group.raw_templates": "Engine Templates", + "streams.group.processed": "Sources", + "streams.group.proc_templates": "Filter Templates", "streams.group.css_processing": "Processing Templates", "streams.group.color_strip": "Color Strips", "streams.group.audio": "Audio", @@ -1213,6 +1215,8 @@ "audio_template.error.delete": "Failed to delete audio template", "streams.group.value": "Value Sources", "streams.group.sync": "Sync Clocks", + "tree.group.capture": "Screen Capture", + "tree.group.processing": "Processed", "tree.group.picture": "Picture", "tree.group.strip": "Color Strip", "tree.group.utility": "Utility", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index 442a860..5880ed7 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -366,8 +366,10 @@ "section.collapse_all": "Свернуть все секции", "streams.title": "Источники", "streams.description": "Источники определяют конвейер захвата. Сырой источник захватывает экран с помощью шаблона захвата. Обработанный источник применяет постобработку к другому источнику. Назначайте источники устройствам.", - "streams.group.raw": "Захват Экрана", - "streams.group.processed": "Обработанные", + "streams.group.raw": "Источники", + "streams.group.raw_templates": "Шаблоны движка", + "streams.group.processed": "Источники", + "streams.group.proc_templates": "Шаблоны фильтров", "streams.group.css_processing": "Шаблоны Обработки", "streams.group.color_strip": "Цветовые Полосы", "streams.group.audio": "Аудио", @@ -1162,6 +1164,8 @@ "audio_template.error.delete": "Не удалось удалить аудиошаблон", "streams.group.value": "Источники значений", "streams.group.sync": "Часы синхронизации", + "tree.group.capture": "Захват Экрана", + "tree.group.processing": "Обработанные", "tree.group.picture": "Изображения", "tree.group.strip": "Цветовые Полосы", "tree.group.utility": "Утилиты", diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index db27e0b..7922d91 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -366,8 +366,10 @@ "section.collapse_all": "全部折叠", "streams.title": "源", "streams.description": "源定义采集管线。原始源使用采集模板从显示器采集。处理源对另一个源应用后处理。将源分配给设备。", - "streams.group.raw": "屏幕采集", - "streams.group.processed": "已处理", + "streams.group.raw": "源", + "streams.group.raw_templates": "引擎模板", + "streams.group.processed": "源", + "streams.group.proc_templates": "滤镜模板", "streams.group.css_processing": "处理模板", "streams.group.color_strip": "色带源", "streams.group.audio": "音频", @@ -1162,6 +1164,8 @@ "audio_template.error.delete": "删除音频模板失败", "streams.group.value": "值源", "streams.group.sync": "同步时钟", + "tree.group.capture": "屏幕采集", + "tree.group.processing": "已处理", "tree.group.picture": "图片", "tree.group.strip": "色带", "tree.group.utility": "工具",