diff --git a/server/src/wled_controller/static/js/features/automations.js b/server/src/wled_controller/static/js/features/automations.js index c1a3bd4..2b42e55 100644 --- a/server/src/wled_controller/static/js/features/automations.js +++ b/server/src/wled_controller/static/js/features/automations.js @@ -10,7 +10,9 @@ import { Modal } from '../core/modal.js'; import { CardSection } from '../core/card-sections.js'; import { updateTabBadge } from './tabs.js'; import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE, ICON_CLONE } from '../core/icons.js'; +import * as P from '../core/icon-paths.js'; import { wrapCard } from '../core/card-colors.js'; +import { IconSelect } from '../core/icon-select.js'; import { attachProcessPicker } from '../core/process-picker.js'; import { csScenes, createSceneCard } from './scene-presets.js'; @@ -33,6 +35,23 @@ class AutomationEditorModal extends Modal { const automationModal = new AutomationEditorModal(); const csAutomations = new CardSection('automations', { titleKey: 'automations.title', gridClass: 'devices-grid', addCardOnclick: "openAutomationEditor()", keyAttr: 'data-automation-id' }); +/* ── Condition logic IconSelect ───────────────────────────────── */ + +const _icon = (d) => `${d}`; + +let _conditionLogicIconSelect = null; + +function _ensureConditionLogicIconSelect() { + const sel = document.getElementById('automation-editor-logic'); + if (!sel) return; + const items = [ + { value: 'or', icon: _icon(P.zap), label: t('automations.condition_logic.or'), desc: t('automations.condition_logic.or.desc') }, + { value: 'and', icon: _icon(P.link), label: t('automations.condition_logic.and'), desc: t('automations.condition_logic.and.desc') }, + ]; + if (_conditionLogicIconSelect) { _conditionLogicIconSelect.updateItems(items); return; } + _conditionLogicIconSelect = new IconSelect({ target: sel, items, columns: 2 }); +} + // Re-render automations when language changes (only if tab is active) document.addEventListener('languageChanged', () => { if (apiKey && (localStorage.getItem('activeTab') || 'dashboard') === 'automations') loadAutomations(); @@ -207,6 +226,8 @@ export async function openAutomationEditor(automationId, cloneData) { errorEl.style.display = 'none'; condList.innerHTML = ''; + _ensureConditionLogicIconSelect(); + // Fetch scenes for selector try { await scenePresetsCache.fetch(); @@ -227,6 +248,7 @@ export async function openAutomationEditor(automationId, cloneData) { nameInput.value = automation.name; enabledInput.checked = automation.enabled; logicSelect.value = automation.condition_logic; + if (_conditionLogicIconSelect) _conditionLogicIconSelect.setValue(automation.condition_logic); for (const c of automation.conditions) { addAutomationConditionRow(c); @@ -250,6 +272,7 @@ export async function openAutomationEditor(automationId, cloneData) { nameInput.value = (cloneData.name || '') + ' (Copy)'; enabledInput.checked = cloneData.enabled !== false; logicSelect.value = cloneData.condition_logic || 'or'; + if (_conditionLogicIconSelect) _conditionLogicIconSelect.setValue(cloneData.condition_logic || 'or'); // Clone conditions (strip webhook tokens — they must be unique) for (const c of (cloneData.conditions || [])) { @@ -269,6 +292,7 @@ export async function openAutomationEditor(automationId, cloneData) { nameInput.value = ''; enabledInput.checked = true; logicSelect.value = 'or'; + if (_conditionLogicIconSelect) _conditionLogicIconSelect.setValue('or'); _initSceneSelector('automation-scene', null); _initSceneSelector('automation-fallback-scene', null); } 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 e6698cd..b15859d 100644 --- a/server/src/wled_controller/static/js/features/kc-targets.js +++ b/server/src/wled_controller/static/js/features/kc-targets.js @@ -19,7 +19,10 @@ import { ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_START, ICON_STOP, ICON_PAUSE, ICON_LINK_SOURCE, ICON_PATTERN_TEMPLATE, ICON_FPS, ICON_PALETTE, } from '../core/icons.js'; +import * as P from '../core/icon-paths.js'; import { wrapCard } from '../core/card-colors.js'; +import { IconSelect } from '../core/icon-select.js'; +import { EntitySelect } from '../core/entity-palette.js'; class KCEditorModal extends Modal { constructor() { @@ -41,6 +44,94 @@ class KCEditorModal extends Modal { const kcEditorModal = new KCEditorModal(); +/* ── Visual selectors ─────────────────────────────────────────── */ + +const _icon = (d) => `${d}`; + +let _kcColorModeIconSelect = null; +let _kcSourceEntitySelect = null; +let _kcPatternEntitySelect = null; +let _kcBrightnessEntitySelect = null; + +// Inline SVG previews for color modes +const _COLOR_MODE_SVG = { + average: '', + median: '', + dominant: '', +}; + +function _ensureColorModeIconSelect() { + const sel = document.getElementById('kc-editor-interpolation'); + if (!sel) return; + const items = [ + { value: 'average', icon: _COLOR_MODE_SVG.average, label: t('kc.interpolation.average'), desc: t('kc.interpolation.average.desc') }, + { value: 'median', icon: _COLOR_MODE_SVG.median, label: t('kc.interpolation.median'), desc: t('kc.interpolation.median.desc') }, + { value: 'dominant', icon: _COLOR_MODE_SVG.dominant, label: t('kc.interpolation.dominant'), desc: t('kc.interpolation.dominant.desc') }, + ]; + if (_kcColorModeIconSelect) { _kcColorModeIconSelect.updateItems(items); return; } + _kcColorModeIconSelect = new IconSelect({ target: sel, items, columns: 3 }); +} + +function _ensureSourceEntitySelect(sources) { + const sel = document.getElementById('kc-editor-source'); + if (!sel) return; + if (_kcSourceEntitySelect) _kcSourceEntitySelect.destroy(); + if (sources.length > 0) { + _kcSourceEntitySelect = new EntitySelect({ + target: sel, + getItems: () => sources.map(s => ({ + value: s.id, + label: s.name, + icon: getPictureSourceIcon(s.stream_type), + desc: s.stream_type, + })), + placeholder: t('palette.search'), + }); + } +} + +function _ensurePatternEntitySelect(patTemplates) { + const sel = document.getElementById('kc-editor-pattern-template'); + if (!sel) return; + if (_kcPatternEntitySelect) _kcPatternEntitySelect.destroy(); + if (patTemplates.length > 0) { + _kcPatternEntitySelect = new EntitySelect({ + target: sel, + getItems: () => patTemplates.map(pt => { + const rectCount = (pt.rectangles || []).length; + return { + value: pt.id, + label: pt.name, + icon: _icon(P.fileText), + desc: `${rectCount} rect${rectCount !== 1 ? 's' : ''}`, + }; + }), + placeholder: t('palette.search'), + }); + } +} + +function _ensureBrightnessEntitySelect() { + const sel = document.getElementById('kc-editor-brightness-vs'); + if (!sel) return; + if (_kcBrightnessEntitySelect) _kcBrightnessEntitySelect.destroy(); + if (_cachedValueSources.length > 0) { + _kcBrightnessEntitySelect = new EntitySelect({ + target: sel, + getItems: () => { + const items = [{ value: '', label: t('kc.brightness_vs.none'), icon: _icon(P.sunDim), desc: '' }]; + return items.concat(_cachedValueSources.map(vs => ({ + value: vs.id, + label: vs.name, + icon: getValueSourceIcon(vs.source_type), + desc: vs.source_type, + }))); + }, + placeholder: t('palette.search'), + }); + } +} + export function patchKCTargetMetrics(target) { const card = document.querySelector(`[data-kc-target-id="${target.id}"]`); if (!card) return; @@ -400,13 +491,13 @@ function _populateKCBrightnessVsDropdown(selectedId = '') { // Keep the first "None" option, remove the rest while (sel.options.length > 1) sel.remove(1); _cachedValueSources.forEach(vs => { - const icon = getValueSourceIcon(vs.source_type); const opt = document.createElement('option'); opt.value = vs.id; opt.textContent = vs.name; sel.appendChild(opt); }); sel.value = selectedId || ''; + _ensureBrightnessEntitySelect(); } export async function showKCEditor(targetId = null, cloneData = null) { @@ -442,6 +533,11 @@ export async function showKCEditor(targetId = null, cloneData = null) { patSelect.appendChild(opt); }); + // Set up visual selectors + _ensureColorModeIconSelect(); + _ensureSourceEntitySelect(sources); + _ensurePatternEntitySelect(patTemplates); + if (targetId) { const resp = await fetch(`${API_BASE}/picture-targets/${targetId}`, { headers: getHeaders() }); if (!resp.ok) throw new Error('Failed to load target'); @@ -454,6 +550,7 @@ export async function showKCEditor(targetId = null, cloneData = null) { document.getElementById('kc-editor-fps').value = kcSettings.fps ?? 10; document.getElementById('kc-editor-fps-value').textContent = kcSettings.fps ?? 10; document.getElementById('kc-editor-interpolation').value = kcSettings.interpolation_mode ?? 'average'; + if (_kcColorModeIconSelect) _kcColorModeIconSelect.setValue(kcSettings.interpolation_mode ?? 'average'); document.getElementById('kc-editor-smoothing').value = kcSettings.smoothing ?? 0.3; document.getElementById('kc-editor-smoothing-value').textContent = kcSettings.smoothing ?? 0.3; patSelect.value = kcSettings.pattern_template_id || ''; @@ -467,6 +564,7 @@ export async function showKCEditor(targetId = null, cloneData = null) { document.getElementById('kc-editor-fps').value = kcSettings.fps ?? 10; document.getElementById('kc-editor-fps-value').textContent = kcSettings.fps ?? 10; document.getElementById('kc-editor-interpolation').value = kcSettings.interpolation_mode ?? 'average'; + if (_kcColorModeIconSelect) _kcColorModeIconSelect.setValue(kcSettings.interpolation_mode ?? 'average'); document.getElementById('kc-editor-smoothing').value = kcSettings.smoothing ?? 0.3; document.getElementById('kc-editor-smoothing-value').textContent = kcSettings.smoothing ?? 0.3; patSelect.value = kcSettings.pattern_template_id || ''; @@ -479,6 +577,7 @@ export async function showKCEditor(targetId = null, cloneData = null) { document.getElementById('kc-editor-fps').value = 10; document.getElementById('kc-editor-fps-value').textContent = '10'; document.getElementById('kc-editor-interpolation').value = 'average'; + if (_kcColorModeIconSelect) _kcColorModeIconSelect.setValue('average'); document.getElementById('kc-editor-smoothing').value = 0.3; document.getElementById('kc-editor-smoothing-value').textContent = '0.3'; if (patTemplates.length > 0) patSelect.value = patTemplates[0].id; diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index d39ec02..5d3ddb7 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -524,6 +524,9 @@ "kc.interpolation.average": "Average", "kc.interpolation.median": "Median", "kc.interpolation.dominant": "Dominant", + "kc.interpolation.average.desc": "Mean of all pixel colors", + "kc.interpolation.median.desc": "Middle color value per channel", + "kc.interpolation.dominant.desc": "Most frequent color", "kc.smoothing": "Smoothing:", "kc.smoothing.hint": "Temporal blending between extractions (0=none, 1=full)", "kc.pattern_template": "Pattern Template:", @@ -614,6 +617,8 @@ "automations.condition_logic.hint": "How multiple conditions are combined: ANY (OR) or ALL (AND)", "automations.condition_logic.or": "Any condition (OR)", "automations.condition_logic.and": "All conditions (AND)", + "automations.condition_logic.or.desc": "Triggers when any condition matches", + "automations.condition_logic.and.desc": "Triggers only when all match", "automations.conditions": "Conditions:", "automations.conditions.hint": "Rules that determine when this automation activates", "automations.conditions.add": "Add Condition", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index d540576..0b5a6b6 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -524,6 +524,9 @@ "kc.interpolation.average": "Среднее", "kc.interpolation.median": "Медиана", "kc.interpolation.dominant": "Доминантный", + "kc.interpolation.average.desc": "Среднее всех цветов пикселей", + "kc.interpolation.median.desc": "Медианное значение по каналам", + "kc.interpolation.dominant.desc": "Наиболее частый цвет", "kc.smoothing": "Сглаживание:", "kc.smoothing.hint": "Временное смешивание между извлечениями (0=нет, 1=полное)", "kc.pattern_template": "Шаблон Паттерна:", @@ -614,6 +617,8 @@ "automations.condition_logic.hint": "Как объединяются несколько условий: ЛЮБОЕ (ИЛИ) или ВСЕ (И)", "automations.condition_logic.or": "Любое условие (ИЛИ)", "automations.condition_logic.and": "Все условия (И)", + "automations.condition_logic.or.desc": "Срабатывает при любом совпадении", + "automations.condition_logic.and.desc": "Срабатывает только при всех", "automations.conditions": "Условия:", "automations.conditions.hint": "Правила, определяющие когда автоматизация активируется", "automations.conditions.add": "Добавить условие", diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index c60803c..795eb57 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -524,6 +524,9 @@ "kc.interpolation.average": "平均", "kc.interpolation.median": "中位数", "kc.interpolation.dominant": "主色", + "kc.interpolation.average.desc": "所有像素颜色的平均值", + "kc.interpolation.median.desc": "每通道中间颜色值", + "kc.interpolation.dominant.desc": "出现最频繁的颜色", "kc.smoothing": "平滑:", "kc.smoothing.hint": "提取间的时间混合(0=无,1=完全)", "kc.pattern_template": "图案模板:", @@ -614,6 +617,8 @@ "automations.condition_logic.hint": "多个条件的组合方式:任一(或)或 全部(与)", "automations.condition_logic.or": "任一条件(或)", "automations.condition_logic.and": "全部条件(与)", + "automations.condition_logic.or.desc": "任一条件匹配时触发", + "automations.condition_logic.and.desc": "全部匹配时才触发", "automations.conditions": "条件:", "automations.conditions.hint": "决定此自动化何时激活的规则", "automations.conditions.add": "添加条件", diff --git a/server/src/wled_controller/static/sw.js b/server/src/wled_controller/static/sw.js index 22b5c8f..2728b8a 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-v22'; +const CACHE_NAME = 'ledgrab-v23'; // Only pre-cache static assets (no auth required). // Do NOT pre-cache '/' — it requires API key auth and would cache an error page.