diff --git a/server/src/wled_controller/static/js/features/color-strips.js b/server/src/wled_controller/static/js/features/color-strips.js index 2a2d7a2..7a6e6d1 100644 --- a/server/src/wled_controller/static/js/features/color-strips.js +++ b/server/src/wled_controller/static/js/features/color-strips.js @@ -14,6 +14,7 @@ import { ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM, ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK, ICON_BELL, } from '../core/icons.js'; +import * as P from '../core/icon-paths.js'; import { wrapCard } from '../core/card-colors.js'; import { attachProcessPicker } from '../core/process-picker.js'; import { IconSelect } from '../core/icon-select.js'; @@ -128,8 +129,18 @@ export function onCSSTypeChange() { document.getElementById('css-editor-api-input-section').style.display = type === 'api_input' ? '' : 'none'; document.getElementById('css-editor-notification-section').style.display = type === 'notification' ? '' : 'none'; - if (type === 'effect') onEffectTypeChange(); - if (type === 'audio') onAudioVizChange(); + if (type === 'effect') { + _ensureEffectTypeIconSelect(); + _ensureEffectPaletteIconSelect(); + onEffectTypeChange(); + } + if (type === 'audio') { + _ensureAudioVizIconSelect(); + _ensureAudioPaletteIconSelect(); + onAudioVizChange(); + } + if (type === 'gradient') _ensureGradientPresetIconSelect(); + if (type === 'notification') _ensureNotificationEffectIconSelect(); // Animation section — shown for static/gradient only const animSection = document.getElementById('css-editor-animation-section'); @@ -240,6 +251,110 @@ function _syncAnimationSpeedState() { } } +/* ── Gradient strip preview helper ────────────────────────────── */ + +/** + * Build a small inline CSS gradient preview from palette color points. + * @param {Array<[number, string]>} pts – [[position, 'r,g,b'], ...] + * @param {number} [w=80] width in px + * @param {number} [h=16] height in px + * @returns {string} HTML string + */ +function _gradientStripHTML(pts, w = 80, h = 16) { + const stops = pts.map(([pos, rgb]) => `rgb(${rgb}) ${(pos * 100).toFixed(0)}%`).join(', '); + return ``; +} + +/** + * Build a gradient preview from _GRADIENT_PRESETS entry (array of {position, color:[r,g,b]}). + */ +function _gradientPresetStripHTML(stops, w = 80, h = 16) { + const css = stops.map(s => `rgb(${s.color.join(',')}) ${(s.position * 100).toFixed(0)}%`).join(', '); + return ``; +} + +/* ── Effect / audio palette IconSelect instances ─────────────── */ + +let _effectTypeIconSelect = null; +let _effectPaletteIconSelect = null; +let _audioPaletteIconSelect = null; +let _audioVizIconSelect = null; +let _gradientPresetIconSelect = null; +let _notificationEffectIconSelect = null; + +const _icon = (d) => `${d}`; + +function _ensureEffectTypeIconSelect() { + const sel = document.getElementById('css-editor-effect-type'); + if (!sel) return; + const items = [ + { value: 'fire', icon: _icon(P.zap), label: t('color_strip.effect.fire'), desc: t('color_strip.effect.fire.desc') }, + { value: 'meteor', icon: _icon(P.sparkles), label: t('color_strip.effect.meteor'), desc: t('color_strip.effect.meteor.desc') }, + { value: 'plasma', icon: _icon(P.rainbow), label: t('color_strip.effect.plasma'), desc: t('color_strip.effect.plasma.desc') }, + { value: 'noise', icon: _icon(P.activity), label: t('color_strip.effect.noise'), desc: t('color_strip.effect.noise.desc') }, + { value: 'aurora', icon: _icon(P.sparkles), label: t('color_strip.effect.aurora'), desc: t('color_strip.effect.aurora.desc') }, + ]; + if (_effectTypeIconSelect) { _effectTypeIconSelect.updateItems(items); return; } + _effectTypeIconSelect = new IconSelect({ target: sel, items, columns: 2 }); +} + +function _ensureEffectPaletteIconSelect() { + const sel = document.getElementById('css-editor-effect-palette'); + if (!sel) return; + const items = Object.entries(_PALETTE_COLORS).map(([key, pts]) => ({ + value: key, icon: _gradientStripHTML(pts), label: t(`color_strip.palette.${key}`), + })); + if (_effectPaletteIconSelect) { _effectPaletteIconSelect.updateItems(items); return; } + _effectPaletteIconSelect = new IconSelect({ target: sel, items, columns: 2 }); +} + +function _ensureAudioPaletteIconSelect() { + const sel = document.getElementById('css-editor-audio-palette'); + if (!sel) return; + const items = Object.entries(_PALETTE_COLORS).map(([key, pts]) => ({ + value: key, icon: _gradientStripHTML(pts), label: t(`color_strip.palette.${key}`), + })); + if (_audioPaletteIconSelect) { _audioPaletteIconSelect.updateItems(items); return; } + _audioPaletteIconSelect = new IconSelect({ target: sel, items, columns: 2 }); +} + +function _ensureAudioVizIconSelect() { + const sel = document.getElementById('css-editor-audio-viz'); + if (!sel) return; + const items = [ + { value: 'spectrum', icon: _icon(P.activity), label: t('color_strip.audio.viz.spectrum'), desc: t('color_strip.audio.viz.spectrum.desc') }, + { value: 'beat_pulse', icon: _icon(P.zap), label: t('color_strip.audio.viz.beat_pulse'), desc: t('color_strip.audio.viz.beat_pulse.desc') }, + { value: 'vu_meter', icon: _icon(P.trendingUp), label: t('color_strip.audio.viz.vu_meter'), desc: t('color_strip.audio.viz.vu_meter.desc') }, + ]; + if (_audioVizIconSelect) { _audioVizIconSelect.updateItems(items); return; } + _audioVizIconSelect = new IconSelect({ target: sel, items, columns: 3 }); +} + +function _ensureGradientPresetIconSelect() { + const sel = document.getElementById('css-editor-gradient-preset'); + if (!sel) return; + const items = [ + { value: '', icon: _icon(P.palette), label: t('color_strip.gradient.preset.custom') }, + ...Object.entries(_GRADIENT_PRESETS).map(([key, stops]) => ({ + value: key, icon: _gradientPresetStripHTML(stops), label: t(`color_strip.gradient.preset.${key}`), + })), + ]; + if (_gradientPresetIconSelect) { _gradientPresetIconSelect.updateItems(items); return; } + _gradientPresetIconSelect = new IconSelect({ target: sel, items, columns: 3 }); +} + +function _ensureNotificationEffectIconSelect() { + const sel = document.getElementById('css-editor-notification-effect'); + if (!sel) return; + const items = [ + { value: 'flash', icon: _icon(P.zap), label: t('color_strip.notification.effect.flash'), desc: t('color_strip.notification.effect.flash.desc') }, + { value: 'pulse', icon: _icon(P.activity), label: t('color_strip.notification.effect.pulse'), desc: t('color_strip.notification.effect.pulse.desc') }, + { value: 'sweep', icon: _icon(P.fastForward), label: t('color_strip.notification.effect.sweep'), desc: t('color_strip.notification.effect.sweep.desc') }, + ]; + if (_notificationEffectIconSelect) { _notificationEffectIconSelect.updateItems(items); return; } + _notificationEffectIconSelect = new IconSelect({ target: sel, items, columns: 3 }); +} + /* ── Effect type helpers ──────────────────────────────────────── */ // Palette color control points — mirrors _PALETTE_DEFS in effect_stream.py @@ -623,6 +738,7 @@ async function _loadAudioSources() { function _loadAudioState(css) { document.getElementById('css-editor-audio-viz').value = css.visualization_mode || 'spectrum'; + if (_audioVizIconSelect) _audioVizIconSelect.setValue(css.visualization_mode || 'spectrum'); onAudioVizChange(); const sensitivity = css.sensitivity ?? 1.0; @@ -634,6 +750,7 @@ function _loadAudioState(css) { document.getElementById('css-editor-audio-smoothing-val').textContent = parseFloat(smoothing).toFixed(2); document.getElementById('css-editor-audio-palette').value = css.palette || 'rainbow'; + if (_audioPaletteIconSelect) _audioPaletteIconSelect.setValue(css.palette || 'rainbow'); document.getElementById('css-editor-audio-color').value = rgbArrayToHex(css.color || [0, 255, 0]); document.getElementById('css-editor-audio-color-peak').value = rgbArrayToHex(css.color_peak || [255, 0, 0]); document.getElementById('css-editor-audio-mirror').checked = css.mirror || false; @@ -647,11 +764,13 @@ function _loadAudioState(css) { function _resetAudioState() { document.getElementById('css-editor-audio-viz').value = 'spectrum'; + if (_audioVizIconSelect) _audioVizIconSelect.setValue('spectrum'); document.getElementById('css-editor-audio-sensitivity').value = 1.0; document.getElementById('css-editor-audio-sensitivity-val').textContent = '1.0'; document.getElementById('css-editor-audio-smoothing').value = 0.3; document.getElementById('css-editor-audio-smoothing-val').textContent = '0.30'; document.getElementById('css-editor-audio-palette').value = 'rainbow'; + if (_audioPaletteIconSelect) _audioPaletteIconSelect.setValue('rainbow'); document.getElementById('css-editor-audio-color').value = '#00ff00'; document.getElementById('css-editor-audio-color-peak').value = '#ff0000'; document.getElementById('css-editor-audio-mirror').checked = false; @@ -715,6 +834,7 @@ function _notificationGetAppColorsDict() { function _loadNotificationState(css) { document.getElementById('css-editor-notification-effect').value = css.notification_effect || 'flash'; + if (_notificationEffectIconSelect) _notificationEffectIconSelect.setValue(css.notification_effect || 'flash'); const dur = css.duration_ms ?? 1500; document.getElementById('css-editor-notification-duration').value = dur; document.getElementById('css-editor-notification-duration-val').textContent = dur; @@ -734,6 +854,7 @@ function _loadNotificationState(css) { function _resetNotificationState() { document.getElementById('css-editor-notification-effect').value = 'flash'; + if (_notificationEffectIconSelect) _notificationEffectIconSelect.setValue('flash'); document.getElementById('css-editor-notification-duration').value = 1500; document.getElementById('css-editor-notification-duration-val').textContent = '1500'; document.getElementById('css-editor-notification-default-color').value = '#ffffff'; @@ -993,6 +1114,7 @@ export async function showCSSEditor(cssId = null, cloneData = null) { _loadColorCycleState(css); } else if (sourceType === 'gradient') { document.getElementById('css-editor-gradient-preset').value = ''; + if (_gradientPresetIconSelect) _gradientPresetIconSelect.setValue(''); gradientInit(css.stops || [ { position: 0.0, color: [255, 0, 0] }, { position: 1.0, color: [0, 0, 255] }, @@ -1000,8 +1122,10 @@ export async function showCSSEditor(cssId = null, cloneData = null) { _loadAnimationState(css.animation); } else if (sourceType === 'effect') { document.getElementById('css-editor-effect-type').value = css.effect_type || 'fire'; + if (_effectTypeIconSelect) _effectTypeIconSelect.setValue(css.effect_type || 'fire'); onEffectTypeChange(); document.getElementById('css-editor-effect-palette').value = css.palette || 'fire'; + if (_effectPaletteIconSelect) _effectPaletteIconSelect.setValue(css.palette || 'fire'); document.getElementById('css-editor-effect-color').value = rgbArrayToHex(css.color || [255, 80, 0]); document.getElementById('css-editor-effect-intensity').value = css.intensity ?? 1.0; document.getElementById('css-editor-effect-intensity-val').textContent = parseFloat(css.intensity ?? 1.0).toFixed(1); 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 8dcac8e..550f25c 100644 --- a/server/src/wled_controller/static/js/features/value-sources.js +++ b/server/src/wled_controller/static/js/features/value-sources.js @@ -78,6 +78,27 @@ function _buildVSTypeItems() { } let _vsTypeIconSelect = null; +let _waveformIconSelect = null; + +const _WAVEFORM_SVG = { + sine: '', + triangle: '', + square: '', + sawtooth: '', +}; + +function _ensureWaveformIconSelect() { + const sel = document.getElementById('value-source-waveform'); + if (!sel) return; + const items = [ + { value: 'sine', icon: _WAVEFORM_SVG.sine, label: t('value_source.waveform.sine') }, + { value: 'triangle', icon: _WAVEFORM_SVG.triangle, label: t('value_source.waveform.triangle') }, + { value: 'square', icon: _WAVEFORM_SVG.square, label: t('value_source.waveform.square') }, + { value: 'sawtooth', icon: _WAVEFORM_SVG.sawtooth, label: t('value_source.waveform.sawtooth') }, + ]; + if (_waveformIconSelect) { _waveformIconSelect.updateItems(items); return; } + _waveformIconSelect = new IconSelect({ target: sel, items, columns: 4 }); +} function _ensureVSTypeIconSelect() { const sel = document.getElementById('value-source-type'); @@ -111,6 +132,7 @@ export async function showValueSourceModal(editData) { _setSlider('value-source-value', editData.value ?? 1.0); } else if (editData.source_type === 'animated') { document.getElementById('value-source-waveform').value = editData.waveform || 'sine'; + if (_waveformIconSelect) _waveformIconSelect.setValue(editData.waveform || 'sine'); _setSlider('value-source-speed', editData.speed ?? 10); _setSlider('value-source-min-value', editData.min_value ?? 0); _setSlider('value-source-max-value', editData.max_value ?? 1); @@ -174,6 +196,7 @@ export function onValueSourceTypeChange() { if (_vsTypeIconSelect) _vsTypeIconSelect.setValue(type); document.getElementById('value-source-static-section').style.display = type === 'static' ? '' : 'none'; 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'; 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'; diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index f9e5aaa..9016bc5 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -840,8 +840,11 @@ "color_strip.notification.effect": "Effect:", "color_strip.notification.effect.hint": "Visual effect when a notification fires. Flash fades linearly, Pulse uses a smooth bell curve, Sweep fills LEDs left-to-right then fades.", "color_strip.notification.effect.flash": "Flash", + "color_strip.notification.effect.flash.desc": "Instant on, linear fade-out", "color_strip.notification.effect.pulse": "Pulse", + "color_strip.notification.effect.pulse.desc": "Smooth bell-curve glow", "color_strip.notification.effect.sweep": "Sweep", + "color_strip.notification.effect.sweep.desc": "Fills left-to-right then fades", "color_strip.notification.duration": "Duration (ms):", "color_strip.notification.duration.hint": "How long the notification effect plays, in milliseconds.", "color_strip.notification.default_color": "Default Color:", @@ -888,8 +891,11 @@ "color_strip.audio.visualization": "Visualization:", "color_strip.audio.visualization.hint": "How audio data is rendered to LEDs.", "color_strip.audio.viz.spectrum": "Spectrum Analyzer", + "color_strip.audio.viz.spectrum.desc": "Frequency bars across the strip", "color_strip.audio.viz.beat_pulse": "Beat Pulse", + "color_strip.audio.viz.beat_pulse.desc": "All LEDs pulse on the beat", "color_strip.audio.viz.vu_meter": "VU Meter", + "color_strip.audio.viz.vu_meter.desc": "Volume level fills the strip", "color_strip.audio.source": "Audio Source:", "color_strip.audio.source.hint": "Audio source for this visualization. Can be a multichannel (device) or mono (single channel) source. Create and manage audio sources in the Sources tab.", "color_strip.audio.sensitivity": "Sensitivity:", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index 3419033..ddc9dd4 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -840,8 +840,11 @@ "color_strip.notification.effect": "Эффект:", "color_strip.notification.effect.hint": "Визуальный эффект при уведомлении. Вспышка — линейное затухание, Пульс — плавная волна, Волна — заполнение и затухание.", "color_strip.notification.effect.flash": "Вспышка", + "color_strip.notification.effect.flash.desc": "Мгновенное включение, линейное затухание", "color_strip.notification.effect.pulse": "Пульс", + "color_strip.notification.effect.pulse.desc": "Плавное свечение колоколом", "color_strip.notification.effect.sweep": "Волна", + "color_strip.notification.effect.sweep.desc": "Заполняет слева направо, затем гаснет", "color_strip.notification.duration": "Длительность (мс):", "color_strip.notification.duration.hint": "Как долго длится эффект уведомления в миллисекундах.", "color_strip.notification.default_color": "Цвет по умолчанию:", @@ -888,8 +891,11 @@ "color_strip.audio.visualization": "Визуализация:", "color_strip.audio.visualization.hint": "Способ отображения аудиоданных на LED.", "color_strip.audio.viz.spectrum": "Анализатор спектра", + "color_strip.audio.viz.spectrum.desc": "Частотные полосы по ленте", "color_strip.audio.viz.beat_pulse": "Пульс бита", + "color_strip.audio.viz.beat_pulse.desc": "Все LED пульсируют в такт", "color_strip.audio.viz.vu_meter": "VU-метр", + "color_strip.audio.viz.vu_meter.desc": "Уровень громкости заполняет ленту", "color_strip.audio.source": "Аудиоисточник:", "color_strip.audio.source.hint": "Аудиоисточник для визуализации. Может быть многоканальным (устройство) или моно (один канал). Создавайте и управляйте аудиоисточниками на вкладке Источники.", "color_strip.audio.sensitivity": "Чувствительность:", diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index 0d8cb01..ab68321 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -840,8 +840,11 @@ "color_strip.notification.effect": "效果:", "color_strip.notification.effect.hint": "通知触发时的视觉效果。闪烁线性衰减,脉冲平滑钟形曲线,扫描从左到右填充后衰减。", "color_strip.notification.effect.flash": "闪烁", + "color_strip.notification.effect.flash.desc": "瞬时点亮,线性衰减", "color_strip.notification.effect.pulse": "脉冲", + "color_strip.notification.effect.pulse.desc": "平滑钟形发光", "color_strip.notification.effect.sweep": "扫描", + "color_strip.notification.effect.sweep.desc": "从左到右填充然后消失", "color_strip.notification.duration": "持续时间(毫秒):", "color_strip.notification.duration.hint": "通知效果播放的时长(毫秒)。", "color_strip.notification.default_color": "默认颜色:", @@ -888,8 +891,11 @@ "color_strip.audio.visualization": "可视化:", "color_strip.audio.visualization.hint": "音频数据如何渲染到 LED。", "color_strip.audio.viz.spectrum": "频谱分析", + "color_strip.audio.viz.spectrum.desc": "频率条分布在灯带上", "color_strip.audio.viz.beat_pulse": "节拍脉冲", + "color_strip.audio.viz.beat_pulse.desc": "所有LED随节拍脉动", "color_strip.audio.viz.vu_meter": "VU 表", + "color_strip.audio.viz.vu_meter.desc": "音量填充灯带", "color_strip.audio.source": "音频源:", "color_strip.audio.source.hint": "此可视化的音频源。可以是多声道(设备)或单声道(单通道)源。在源标签页中创建和管理音频源。", "color_strip.audio.sensitivity": "灵敏度:", diff --git a/server/src/wled_controller/static/sw.js b/server/src/wled_controller/static/sw.js index ed6b1dd..acb4f8b 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-v18'; +const CACHE_NAME = 'ledgrab-v19'; // Only pre-cache static assets (no auth required). // Do NOT pre-cache '/' — it requires API key auth and would cache an error page.