Add IconSelect grids for animation type and protocol selectors
Replace plain dropdowns with visual icon grids: - Animation type (static/gradient CSS sources): icons for each effect - WLED target protocol: DDP vs HTTP with descriptions Add i18n keys for protocol options in all 3 locales. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -179,27 +179,15 @@ export function onCSSTypeChange() {
|
|||||||
// Animation section — shown for static/gradient only
|
// Animation section — shown for static/gradient only
|
||||||
const animSection = document.getElementById('css-editor-animation-section');
|
const animSection = document.getElementById('css-editor-animation-section');
|
||||||
const animTypeSelect = document.getElementById('css-editor-animation-type');
|
const animTypeSelect = document.getElementById('css-editor-animation-type');
|
||||||
const noneOpt = `<option value="none">${t('color_strip.animation.type.none')}</option>`;
|
if (type === 'static' || type === 'gradient') {
|
||||||
if (type === 'static') {
|
|
||||||
animSection.style.display = '';
|
animSection.style.display = '';
|
||||||
animTypeSelect.innerHTML = noneOpt +
|
const opts = type === 'gradient'
|
||||||
`<option value="breathing">${t('color_strip.animation.type.breathing')}</option>` +
|
? ['none','breathing','gradient_shift','wave','strobe','sparkle','pulse','candle','rainbow_fade']
|
||||||
`<option value="strobe">${t('color_strip.animation.type.strobe')}</option>` +
|
: ['none','breathing','strobe','sparkle','pulse','candle','rainbow_fade'];
|
||||||
`<option value="sparkle">${t('color_strip.animation.type.sparkle')}</option>` +
|
animTypeSelect.innerHTML = opts.map(v =>
|
||||||
`<option value="pulse">${t('color_strip.animation.type.pulse')}</option>` +
|
`<option value="${v}">${t('color_strip.animation.type.' + v)}</option>`
|
||||||
`<option value="candle">${t('color_strip.animation.type.candle')}</option>` +
|
).join('');
|
||||||
`<option value="rainbow_fade">${t('color_strip.animation.type.rainbow_fade')}</option>`;
|
_ensureAnimationTypeIconSelect(type);
|
||||||
} else if (type === 'gradient') {
|
|
||||||
animSection.style.display = '';
|
|
||||||
animTypeSelect.innerHTML = noneOpt +
|
|
||||||
`<option value="breathing">${t('color_strip.animation.type.breathing')}</option>` +
|
|
||||||
`<option value="gradient_shift">${t('color_strip.animation.type.gradient_shift')}</option>` +
|
|
||||||
`<option value="wave">${t('color_strip.animation.type.wave')}</option>` +
|
|
||||||
`<option value="strobe">${t('color_strip.animation.type.strobe')}</option>` +
|
|
||||||
`<option value="sparkle">${t('color_strip.animation.type.sparkle')}</option>` +
|
|
||||||
`<option value="pulse">${t('color_strip.animation.type.pulse')}</option>` +
|
|
||||||
`<option value="candle">${t('color_strip.animation.type.candle')}</option>` +
|
|
||||||
`<option value="rainbow_fade">${t('color_strip.animation.type.rainbow_fade')}</option>`;
|
|
||||||
} else {
|
} else {
|
||||||
animSection.style.display = 'none';
|
animSection.style.display = 'none';
|
||||||
}
|
}
|
||||||
@@ -265,15 +253,14 @@ function _getAnimationPayload() {
|
|||||||
|
|
||||||
function _loadAnimationState(anim) {
|
function _loadAnimationState(anim) {
|
||||||
// Set type after onCSSTypeChange() has populated the dropdown
|
// Set type after onCSSTypeChange() has populated the dropdown
|
||||||
if (anim && anim.enabled && anim.type) {
|
const val = (anim && anim.enabled && anim.type) ? anim.type : 'none';
|
||||||
document.getElementById('css-editor-animation-type').value = anim.type;
|
document.getElementById('css-editor-animation-type').value = val;
|
||||||
} else {
|
if (_animationTypeIconSelect) _animationTypeIconSelect.setValue(val);
|
||||||
document.getElementById('css-editor-animation-type').value = 'none';
|
|
||||||
}
|
|
||||||
_syncAnimationSpeedState();
|
_syncAnimationSpeedState();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onAnimationTypeChange() {
|
export function onAnimationTypeChange() {
|
||||||
|
if (_animationTypeIconSelect) _animationTypeIconSelect.setValue(document.getElementById('css-editor-animation-type').value);
|
||||||
_syncAnimationSpeedState();
|
_syncAnimationSpeedState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,6 +303,7 @@ function _gradientStripHTML(pts, w = 80, h = 16) {
|
|||||||
|
|
||||||
/* ── Effect / audio palette IconSelect instances ─────────────── */
|
/* ── Effect / audio palette IconSelect instances ─────────────── */
|
||||||
|
|
||||||
|
let _animationTypeIconSelect = null;
|
||||||
let _interpolationIconSelect = null;
|
let _interpolationIconSelect = null;
|
||||||
let _effectTypeIconSelect = null;
|
let _effectTypeIconSelect = null;
|
||||||
let _effectPaletteIconSelect = null;
|
let _effectPaletteIconSelect = null;
|
||||||
@@ -422,6 +410,36 @@ function _ensureNotificationFilterModeIconSelect() {
|
|||||||
_notificationFilterModeIconSelect = new IconSelect({ target: sel, items, columns: 3 });
|
_notificationFilterModeIconSelect = new IconSelect({ target: sel, items, columns: 3 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _buildAnimationTypeItems(cssType) {
|
||||||
|
const items = [
|
||||||
|
{ value: 'none', icon: _icon(P.square), label: t('color_strip.animation.type.none'), desc: t('color_strip.animation.type.none.desc') },
|
||||||
|
{ value: 'breathing', icon: _icon(P.activity), label: t('color_strip.animation.type.breathing'), desc: t('color_strip.animation.type.breathing.desc') },
|
||||||
|
];
|
||||||
|
if (cssType === 'gradient') {
|
||||||
|
items.push(
|
||||||
|
{ value: 'gradient_shift', icon: _icon(P.fastForward), label: t('color_strip.animation.type.gradient_shift'), desc: t('color_strip.animation.type.gradient_shift.desc') },
|
||||||
|
{ value: 'wave', icon: _icon(P.rainbow), label: t('color_strip.animation.type.wave'), desc: t('color_strip.animation.type.wave.desc') },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
items.push(
|
||||||
|
{ value: 'strobe', icon: _icon(P.zap), label: t('color_strip.animation.type.strobe'), desc: t('color_strip.animation.type.strobe.desc') },
|
||||||
|
{ value: 'sparkle', icon: _icon(P.sparkles), label: t('color_strip.animation.type.sparkle'), desc: t('color_strip.animation.type.sparkle.desc') },
|
||||||
|
{ value: 'pulse', icon: _icon(P.trendingUp),label: t('color_strip.animation.type.pulse'), desc: t('color_strip.animation.type.pulse.desc') },
|
||||||
|
{ value: 'candle', icon: _icon(P.flame), label: t('color_strip.animation.type.candle'), desc: t('color_strip.animation.type.candle.desc') },
|
||||||
|
{ value: 'rainbow_fade', icon: _icon(P.rainbow), label: t('color_strip.animation.type.rainbow_fade'), desc: t('color_strip.animation.type.rainbow_fade.desc') },
|
||||||
|
);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _ensureAnimationTypeIconSelect(cssType) {
|
||||||
|
const sel = document.getElementById('css-editor-animation-type');
|
||||||
|
if (!sel) return;
|
||||||
|
const items = _buildAnimationTypeItems(cssType);
|
||||||
|
// Destroy and recreate — options change between static/gradient
|
||||||
|
if (_animationTypeIconSelect) { _animationTypeIconSelect.destroy(); _animationTypeIconSelect = null; }
|
||||||
|
_animationTypeIconSelect = new IconSelect({ target: sel, items, columns: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Effect type helpers ──────────────────────────────────────── */
|
/* ── Effect type helpers ──────────────────────────────────────── */
|
||||||
|
|
||||||
// Palette color control points — mirrors _PALETTE_DEFS in effect_stream.py
|
// Palette color control points — mirrors _PALETTE_DEFS in effect_stream.py
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import {
|
|||||||
ICON_WARNING, ICON_PALETTE, ICON_WRENCH, ICON_TEMPLATE,
|
ICON_WARNING, ICON_PALETTE, ICON_WRENCH, ICON_TEMPLATE,
|
||||||
} from '../core/icons.js';
|
} from '../core/icons.js';
|
||||||
import { EntitySelect } from '../core/entity-palette.js';
|
import { EntitySelect } from '../core/entity-palette.js';
|
||||||
|
import { IconSelect } from '../core/icon-select.js';
|
||||||
|
import * as P from '../core/icon-paths.js';
|
||||||
import { wrapCard } from '../core/card-colors.js';
|
import { wrapCard } from '../core/card-colors.js';
|
||||||
import { TagInput, renderTagChips } from '../core/tag-input.js';
|
import { TagInput, renderTagChips } from '../core/tag-input.js';
|
||||||
import { CardSection } from '../core/card-sections.js';
|
import { CardSection } from '../core/card-sections.js';
|
||||||
@@ -246,6 +248,7 @@ function _updateBrightnessThresholdVisibility() {
|
|||||||
let _deviceEntitySelect = null;
|
let _deviceEntitySelect = null;
|
||||||
let _cssEntitySelect = null;
|
let _cssEntitySelect = null;
|
||||||
let _brightnessVsEntitySelect = null;
|
let _brightnessVsEntitySelect = null;
|
||||||
|
let _protocolIconSelect = null;
|
||||||
|
|
||||||
function _populateCssDropdown(selectedId = '') {
|
function _populateCssDropdown(selectedId = '') {
|
||||||
const select = document.getElementById('target-editor-css-source');
|
const select = document.getElementById('target-editor-css-source');
|
||||||
@@ -306,6 +309,19 @@ function _ensureTargetEntitySelects() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _pIcon = (d) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
|
||||||
|
|
||||||
|
function _ensureProtocolIconSelect() {
|
||||||
|
const sel = document.getElementById('target-editor-protocol');
|
||||||
|
if (!sel) return;
|
||||||
|
const items = [
|
||||||
|
{ value: 'ddp', icon: _pIcon(P.radio), label: t('targets.protocol.ddp'), desc: t('targets.protocol.ddp.desc') },
|
||||||
|
{ value: 'http', icon: _pIcon(P.globe), label: t('targets.protocol.http'), desc: t('targets.protocol.http.desc') },
|
||||||
|
];
|
||||||
|
if (_protocolIconSelect) { _protocolIconSelect.updateItems(items); return; }
|
||||||
|
_protocolIconSelect = new IconSelect({ target: sel, items, columns: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
export async function showTargetEditor(targetId = null, cloneData = null) {
|
export async function showTargetEditor(targetId = null, cloneData = null) {
|
||||||
try {
|
try {
|
||||||
// Load devices, CSS sources, and value sources for dropdowns
|
// Load devices, CSS sources, and value sources for dropdowns
|
||||||
@@ -402,6 +418,8 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
|
|||||||
|
|
||||||
// Entity palette selectors
|
// Entity palette selectors
|
||||||
_ensureTargetEntitySelects();
|
_ensureTargetEntitySelects();
|
||||||
|
_ensureProtocolIconSelect();
|
||||||
|
if (_protocolIconSelect) _protocolIconSelect.setValue(document.getElementById('target-editor-protocol').value);
|
||||||
|
|
||||||
// Auto-name generation
|
// Auto-name generation
|
||||||
_targetNameManuallyEdited = !!(targetId || cloneData);
|
_targetNameManuallyEdited = !!(targetId || cloneData);
|
||||||
|
|||||||
@@ -1213,6 +1213,10 @@
|
|||||||
"targets.adaptive_fps.hint": "Automatically reduce send rate when the device becomes unresponsive, and gradually recover when it stabilizes. Recommended for WiFi devices with weak signal.",
|
"targets.adaptive_fps.hint": "Automatically reduce send rate when the device becomes unresponsive, and gradually recover when it stabilizes. Recommended for WiFi devices with weak signal.",
|
||||||
"targets.protocol": "Protocol:",
|
"targets.protocol": "Protocol:",
|
||||||
"targets.protocol.hint": "DDP sends pixels via fast UDP (recommended for most setups). HTTP uses the JSON API — slower but reliable, limited to ~500 LEDs.",
|
"targets.protocol.hint": "DDP sends pixels via fast UDP (recommended for most setups). HTTP uses the JSON API — slower but reliable, limited to ~500 LEDs.",
|
||||||
|
"targets.protocol.ddp": "DDP (UDP)",
|
||||||
|
"targets.protocol.ddp.desc": "Fast raw UDP packets — recommended",
|
||||||
|
"targets.protocol.http": "HTTP",
|
||||||
|
"targets.protocol.http.desc": "JSON API — slower, ≤500 LEDs",
|
||||||
"targets.protocol.serial": "Serial",
|
"targets.protocol.serial": "Serial",
|
||||||
"search.open": "Search (Ctrl+K)",
|
"search.open": "Search (Ctrl+K)",
|
||||||
"search.placeholder": "Search entities... (Ctrl+K)",
|
"search.placeholder": "Search entities... (Ctrl+K)",
|
||||||
|
|||||||
@@ -1213,6 +1213,10 @@
|
|||||||
"targets.adaptive_fps.hint": "Автоматически снижает частоту отправки, когда устройство перестаёт отвечать, и постепенно восстанавливает её при стабилизации. Рекомендуется для WiFi-устройств со слабым сигналом.",
|
"targets.adaptive_fps.hint": "Автоматически снижает частоту отправки, когда устройство перестаёт отвечать, и постепенно восстанавливает её при стабилизации. Рекомендуется для WiFi-устройств со слабым сигналом.",
|
||||||
"targets.protocol": "Протокол:",
|
"targets.protocol": "Протокол:",
|
||||||
"targets.protocol.hint": "DDP отправляет пиксели по быстрому UDP (рекомендуется). HTTP использует JSON API — медленнее, но надёжнее, ограничение ~500 LED.",
|
"targets.protocol.hint": "DDP отправляет пиксели по быстрому UDP (рекомендуется). HTTP использует JSON API — медленнее, но надёжнее, ограничение ~500 LED.",
|
||||||
|
"targets.protocol.ddp": "DDP (UDP)",
|
||||||
|
"targets.protocol.ddp.desc": "Быстрые UDP-пакеты — рекомендуется",
|
||||||
|
"targets.protocol.http": "HTTP",
|
||||||
|
"targets.protocol.http.desc": "JSON API — медленнее, ≤500 LED",
|
||||||
"targets.protocol.serial": "Serial",
|
"targets.protocol.serial": "Serial",
|
||||||
"search.open": "Поиск (Ctrl+K)",
|
"search.open": "Поиск (Ctrl+K)",
|
||||||
"search.placeholder": "Поиск... (Ctrl+K)",
|
"search.placeholder": "Поиск... (Ctrl+K)",
|
||||||
|
|||||||
@@ -1213,6 +1213,10 @@
|
|||||||
"targets.adaptive_fps.hint": "当设备无响应时自动降低发送速率,稳定后逐步恢复。推荐用于信号较弱的WiFi设备。",
|
"targets.adaptive_fps.hint": "当设备无响应时自动降低发送速率,稳定后逐步恢复。推荐用于信号较弱的WiFi设备。",
|
||||||
"targets.protocol": "协议:",
|
"targets.protocol": "协议:",
|
||||||
"targets.protocol.hint": "DDP通过快速UDP发送像素(推荐)。HTTP使用JSON API——较慢但可靠,限制约500个LED。",
|
"targets.protocol.hint": "DDP通过快速UDP发送像素(推荐)。HTTP使用JSON API——较慢但可靠,限制约500个LED。",
|
||||||
|
"targets.protocol.ddp": "DDP (UDP)",
|
||||||
|
"targets.protocol.ddp.desc": "快速UDP数据包 - 推荐",
|
||||||
|
"targets.protocol.http": "HTTP",
|
||||||
|
"targets.protocol.http.desc": "JSON API - 较慢,≤500 LED",
|
||||||
"targets.protocol.serial": "串口",
|
"targets.protocol.serial": "串口",
|
||||||
"search.open": "搜索 (Ctrl+K)",
|
"search.open": "搜索 (Ctrl+K)",
|
||||||
"search.placeholder": "搜索实体... (Ctrl+K)",
|
"search.placeholder": "搜索实体... (Ctrl+K)",
|
||||||
|
|||||||
Reference in New Issue
Block a user