From 0984a3b6391366bb65896cd032cd11044d72c69d Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Mon, 9 Mar 2026 01:01:49 +0300 Subject: [PATCH] Add IconSelect for filter types, audio modes, engine descriptions; fix scroll flash - Filter type picker: IconSelect with 3-column grid, auto-add on select, removed redundant + button - Audio mode picker: IconSelect with SVG visualizations for RMS/Peak/Beat - Capture engine grid: added per-engine icons and localized descriptions - Fixed scroll flash during icon grid open animation (settled class after transitionend) Co-Authored-By: Claude Opus 4.6 --- .../wled_controller/static/css/components.css | 3 +++ .../wled_controller/static/css/streams.css | 3 --- .../static/js/core/icon-select.js | 20 ++++++++++++--- .../wled_controller/static/js/core/icons.js | 5 +++- .../static/js/features/streams.js | 9 +++---- .../static/js/features/value-sources.js | 25 +++++++++++++++++++ .../wled_controller/static/locales/en.json | 9 +++++++ .../wled_controller/static/locales/ru.json | 9 +++++++ .../wled_controller/static/locales/zh.json | 9 +++++++ server/src/wled_controller/static/sw.js | 2 +- .../templates/modals/pp-template.html | 1 - 11 files changed, 80 insertions(+), 15 deletions(-) diff --git a/server/src/wled_controller/static/css/components.css b/server/src/wled_controller/static/css/components.css index baa5135..5063ad1 100644 --- a/server/src/wled_controller/static/css/components.css +++ b/server/src/wled_controller/static/css/components.css @@ -522,6 +522,9 @@ textarea:focus-visible { opacity: 1; margin-top: 6px; } +.icon-select-popup.open.settled { + overflow-y: auto; +} .icon-select-grid { display: grid; diff --git a/server/src/wled_controller/static/css/streams.css b/server/src/wled_controller/static/css/streams.css index fb741ad..95d9735 100644 --- a/server/src/wled_controller/static/css/streams.css +++ b/server/src/wled_controller/static/css/streams.css @@ -459,9 +459,6 @@ body.pp-filter-dragging .pp-filter-drag-handle { } .pp-add-filter-row { - display: flex; - gap: 8px; - align-items: center; margin-bottom: 4px; } diff --git a/server/src/wled_controller/static/js/core/icon-select.js b/server/src/wled_controller/static/js/core/icon-select.js index 3f55307..43f99c5 100644 --- a/server/src/wled_controller/static/js/core/icon-select.js +++ b/server/src/wled_controller/static/js/core/icon-select.js @@ -25,7 +25,7 @@ const POPUP_CLASS = 'icon-select-popup'; /** Close every open icon-select popup. */ export function closeAllIconSelects() { document.querySelectorAll(`.${POPUP_CLASS}`).forEach(p => { - p.classList.remove('open'); + p.classList.remove('open', 'settled'); }); } @@ -52,13 +52,14 @@ export class IconSelect { * @param {Function} [opts.onChange] - called with (value) after user picks * @param {number} [opts.columns=2] - grid column count */ - constructor({ target, items, onChange, columns = 2 }) { + constructor({ target, items, onChange, columns = 2, placeholder = '' }) { _ensureGlobalListener(); this._select = target; this._items = items; this._onChange = onChange; this._columns = columns; + this._placeholder = placeholder; // Hide the native select this._select.style.display = 'none'; @@ -77,6 +78,7 @@ export class IconSelect { this._popup = document.createElement('div'); this._popup.className = POPUP_CLASS; this._popup.addEventListener('click', (e) => e.stopPropagation()); + this._popup.addEventListener('transitionend', this._onTransitionEnd); this._popup.innerHTML = this._buildGrid(); this._select.parentNode.insertBefore(this._popup, this._trigger.nextSibling); @@ -84,7 +86,7 @@ export class IconSelect { this._popup.querySelectorAll('.icon-select-cell').forEach(cell => { cell.addEventListener('click', () => { this.setValue(cell.dataset.value, true); - this._popup.classList.remove('open'); + this._popup.classList.remove('open', 'settled'); }); }); @@ -112,6 +114,10 @@ export class IconSelect { `${item.icon}` + `${item.label}` + ``; + } else if (this._placeholder) { + this._trigger.innerHTML = + `${this._placeholder}` + + ``; } // Update active state in grid this._popup.querySelectorAll('.icon-select-cell').forEach(cell => { @@ -127,6 +133,12 @@ export class IconSelect { } } + _onTransitionEnd = (e) => { + if (e.propertyName === 'max-height' && this._popup.classList.contains('open')) { + this._popup.classList.add('settled'); + } + }; + /** Change the value programmatically. */ setValue(value, fireChange = false) { this._select.value = value; @@ -145,7 +157,7 @@ export class IconSelect { this._popup.querySelectorAll('.icon-select-cell').forEach(cell => { cell.addEventListener('click', () => { this.setValue(cell.dataset.value, true); - this._popup.classList.remove('open'); + this._popup.classList.remove('open', 'settled'); }); }); this._syncTrigger(); diff --git a/server/src/wled_controller/static/js/core/icons.js b/server/src/wled_controller/static/js/core/icons.js index d698b36..e8d2fa8 100644 --- a/server/src/wled_controller/static/js/core/icons.js +++ b/server/src/wled_controller/static/js/core/icons.js @@ -34,7 +34,10 @@ const _deviceTypeIcons = { mqtt: _svg(P.send), ws: _svg(P.globe), openrgb: _svg(P.palette), mock: _svg(P.wrench), }; -const _engineTypeIcons = { scrcpy: _svg(P.smartphone) }; +const _engineTypeIcons = { + mss: _svg(P.monitor), dxcam: _svg(P.zap), bettercam: _svg(P.rocket), + camera: _svg(P.camera), scrcpy: _svg(P.smartphone), wgc: _svg(P.film), +}; const _audioEngineTypeIcons = { wasapi: _svg(P.volume2), sounddevice: _svg(P.mic) }; // ── Type-resolution getters ───────────────────────────────── diff --git a/server/src/wled_controller/static/js/features/streams.js b/server/src/wled_controller/static/js/features/streams.js index f33949f..34838d7 100644 --- a/server/src/wled_controller/static/js/features/streams.js +++ b/server/src/wled_controller/static/js/features/streams.js @@ -303,7 +303,7 @@ async function loadAvailableEngines() { // Update icon-grid selector with dynamic engine list const items = availableEngines .filter(e => e.available) - .map(e => ({ value: e.type, icon: getEngineIcon(e.type), label: e.name, desc: '' })); + .map(e => ({ value: e.type, icon: getEngineIcon(e.type), label: e.name, desc: t(`templates.engine.${e.type}.desc`) })); if (_engineIconSelect) { _engineIconSelect.updateItems(items); } else { _engineIconSelect = new IconSelect({ target: select, items, columns: 2 }); } _engineIconSelect.setValue(select.value); @@ -2026,9 +2026,7 @@ const _FILTER_ICONS = { function _populateFilterSelect() { const select = document.getElementById('pp-add-filter-select'); select.innerHTML = ``; - const items = [ - { value: '', icon: `${P.wrench}`, label: t('filters.select_type') }, - ]; + const items = []; for (const f of _availableFilters) { const name = _getFilterName(f.filter_id); select.innerHTML += ``; @@ -2046,7 +2044,8 @@ function _populateFilterSelect() { _filterIconSelect = new IconSelect({ target: select, items, - columns: 2, + columns: 3, + placeholder: t('filters.select_type'), onChange: () => addFilterFromSelect(), }); } diff --git a/server/src/wled_controller/static/js/features/value-sources.js b/server/src/wled_controller/static/js/features/value-sources.js index 550f25c..fe4762f 100644 --- a/server/src/wled_controller/static/js/features/value-sources.js +++ b/server/src/wled_controller/static/js/features/value-sources.js @@ -100,6 +100,28 @@ function _ensureWaveformIconSelect() { _waveformIconSelect = new IconSelect({ target: sel, items, columns: 4 }); } +/* ── Audio mode icon-grid selector ────────────────────────────── */ + +const _AUDIO_MODE_SVG = { + rms: '', + peak: '', + beat: '', +}; + +let _audioModeIconSelect = null; + +function _ensureAudioModeIconSelect() { + const sel = document.getElementById('value-source-mode'); + if (!sel) return; + const items = [ + { value: 'rms', icon: _AUDIO_MODE_SVG.rms, label: t('value_source.mode.rms'), desc: t('value_source.mode.rms.desc') }, + { value: 'peak', icon: _AUDIO_MODE_SVG.peak, label: t('value_source.mode.peak'), desc: t('value_source.mode.peak.desc') }, + { value: 'beat', icon: _AUDIO_MODE_SVG.beat, label: t('value_source.mode.beat'), desc: t('value_source.mode.beat.desc') }, + ]; + if (_audioModeIconSelect) { _audioModeIconSelect.updateItems(items); return; } + _audioModeIconSelect = new IconSelect({ target: sel, items, columns: 3 }); +} + function _ensureVSTypeIconSelect() { const sel = document.getElementById('value-source-type'); if (!sel) return; @@ -139,6 +161,7 @@ export async function showValueSourceModal(editData) { } else if (editData.source_type === 'audio') { _populateAudioSourceDropdown(editData.audio_source_id || ''); document.getElementById('value-source-mode').value = editData.mode || 'rms'; + if (_audioModeIconSelect) _audioModeIconSelect.setValue(editData.mode || 'rms'); document.getElementById('value-source-auto-gain').checked = !!editData.auto_gain; _setSlider('value-source-sensitivity', editData.sensitivity ?? 1.0); _setSlider('value-source-smoothing', editData.smoothing ?? 0.3); @@ -168,6 +191,7 @@ export async function showValueSourceModal(editData) { document.getElementById('value-source-waveform').value = 'sine'; _populateAudioSourceDropdown(''); document.getElementById('value-source-mode').value = 'rms'; + if (_audioModeIconSelect) _audioModeIconSelect.setValue('rms'); document.getElementById('value-source-auto-gain').checked = false; _setSlider('value-source-sensitivity', 1.0); _setSlider('value-source-smoothing', 0.3); @@ -198,6 +222,7 @@ export function onValueSourceTypeChange() { document.getElementById('value-source-animated-section').style.display = type === 'animated' ? '' : 'none'; if (type === 'animated') _ensureWaveformIconSelect(); document.getElementById('value-source-audio-section').style.display = type === 'audio' ? '' : 'none'; + if (type === 'audio') _ensureAudioModeIconSelect(); document.getElementById('value-source-adaptive-time-section').style.display = type === 'adaptive_time' ? '' : 'none'; document.getElementById('value-source-adaptive-scene-section').style.display = type === 'adaptive_scene' ? '' : 'none'; document.getElementById('value-source-adaptive-range-section').style.display = diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index 3394a04..d39ec02 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -63,6 +63,12 @@ "templates.engine.select": "Select an engine...", "templates.engine.unavailable": "Unavailable", "templates.engine.unavailable.hint": "This engine is not available on your system", + "templates.engine.mss.desc": "Cross-platform, pure Python", + "templates.engine.dxcam.desc": "DirectX, low latency", + "templates.engine.bettercam.desc": "DirectX, high performance", + "templates.engine.camera.desc": "USB/IP camera capture", + "templates.engine.scrcpy.desc": "Android screen mirror", + "templates.engine.wgc.desc": "Windows Graphics Capture", "templates.config": "Configuration", "templates.config.show": "Show configuration", "templates.config.none": "No additional configuration", @@ -1063,6 +1069,9 @@ "value_source.mode.rms": "RMS (Volume)", "value_source.mode.peak": "Peak", "value_source.mode.beat": "Beat", + "value_source.mode.rms.desc": "Average volume level", + "value_source.mode.peak.desc": "Loudest moment tracking", + "value_source.mode.beat.desc": "Rhythm pulse detection", "value_source.auto_gain": "Auto Gain:", "value_source.auto_gain.hint": "Automatically normalize audio levels so output uses the full range, regardless of input volume", "value_source.auto_gain.enable": "Enable auto-gain", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index 4c1fb95..d540576 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -63,6 +63,12 @@ "templates.engine.select": "Выберите движок...", "templates.engine.unavailable": "Недоступен", "templates.engine.unavailable.hint": "Этот движок недоступен в вашей системе", + "templates.engine.mss.desc": "Кроссплатформенный, чистый Python", + "templates.engine.dxcam.desc": "DirectX, низкая задержка", + "templates.engine.bettercam.desc": "DirectX, высокая производительность", + "templates.engine.camera.desc": "Захват USB/IP камеры", + "templates.engine.scrcpy.desc": "Зеркалирование экрана Android", + "templates.engine.wgc.desc": "Windows Graphics Capture", "templates.config": "Конфигурация", "templates.config.show": "Показать конфигурацию", "templates.config.none": "Нет дополнительных настроек", @@ -1063,6 +1069,9 @@ "value_source.mode.rms": "RMS (Громкость)", "value_source.mode.peak": "Пик", "value_source.mode.beat": "Бит", + "value_source.mode.rms.desc": "Средний уровень громкости", + "value_source.mode.peak.desc": "Отслеживание пиковых моментов", + "value_source.mode.beat.desc": "Детекция ритмических ударов", "value_source.auto_gain": "Авто-усиление:", "value_source.auto_gain.hint": "Автоматически нормализует уровни звука, чтобы выходное значение использовало полный диапазон независимо от громкости входного сигнала", "value_source.auto_gain.enable": "Включить авто-усиление", diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index e1f6317..c60803c 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -63,6 +63,12 @@ "templates.engine.select": "选择引擎...", "templates.engine.unavailable": "不可用", "templates.engine.unavailable.hint": "此引擎在您的系统上不可用", + "templates.engine.mss.desc": "跨平台,纯Python", + "templates.engine.dxcam.desc": "DirectX,低延迟", + "templates.engine.bettercam.desc": "DirectX,高性能", + "templates.engine.camera.desc": "USB/IP摄像头捕获", + "templates.engine.scrcpy.desc": "Android屏幕镜像", + "templates.engine.wgc.desc": "Windows图形捕获", "templates.config": "配置", "templates.config.show": "显示配置", "templates.config.none": "无额外配置", @@ -1063,6 +1069,9 @@ "value_source.mode.rms": "RMS(音量)", "value_source.mode.peak": "峰值", "value_source.mode.beat": "节拍", + "value_source.mode.rms.desc": "平均音量水平", + "value_source.mode.peak.desc": "最响时刻追踪", + "value_source.mode.beat.desc": "节奏脉冲检测", "value_source.auto_gain": "自动增益:", "value_source.auto_gain.hint": "自动归一化音频电平,使输出使用完整范围,无论输入音量大小", "value_source.auto_gain.enable": "启用自动增益", diff --git a/server/src/wled_controller/static/sw.js b/server/src/wled_controller/static/sw.js index 2604068..23e6bca 100644 --- a/server/src/wled_controller/static/sw.js +++ b/server/src/wled_controller/static/sw.js @@ -7,7 +7,7 @@ * - Navigation: network-first with offline fallback */ -const CACHE_NAME = 'ledgrab-v20'; +const CACHE_NAME = 'ledgrab-v21'; // Only pre-cache static assets (no auth required). // Do NOT pre-cache '/' — it requires API key auth and would cache an error page. diff --git a/server/src/wled_controller/templates/modals/pp-template.html b/server/src/wled_controller/templates/modals/pp-template.html index 6b5b2b2..1a152d5 100644 --- a/server/src/wled_controller/templates/modals/pp-template.html +++ b/server/src/wled_controller/templates/modals/pp-template.html @@ -21,7 +21,6 @@ -