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) => ``;
+
+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) => ``;
+
+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.