Add visual selectors to automation and KC target editors

Automation editor:
- IconSelect grid for condition logic (OR/AND) with descriptions

KC target editor:
- IconSelect for color mode (average/median/dominant) with SVG previews
- EntitySelect palette for picture source, pattern template, brightness source

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 10:12:57 +03:00
parent 8061c26bef
commit 5b4813368b
6 changed files with 140 additions and 2 deletions

View File

@@ -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) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
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);
}

View File

@@ -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) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
let _kcColorModeIconSelect = null;
let _kcSourceEntitySelect = null;
let _kcPatternEntitySelect = null;
let _kcBrightnessEntitySelect = null;
// Inline SVG previews for color modes
const _COLOR_MODE_SVG = {
average: '<svg viewBox="0 0 60 24" width="60" height="24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="52" height="16" rx="3" opacity="0.3" fill="currentColor"/><path d="M30 8v8" stroke-width="1.5"/><path d="M20 10v4" stroke-width="1.5"/><path d="M40 10v4" stroke-width="1.5"/></svg>',
median: '<svg viewBox="0 0 60 24" width="60" height="24" fill="none" stroke="currentColor" stroke-width="2"><rect x="6" y="14" width="8" height="6" rx="1" fill="currentColor" opacity="0.3"/><rect x="18" y="8" width="8" height="12" rx="1" fill="currentColor" opacity="0.5"/><rect x="30" y="4" width="8" height="16" rx="1" fill="currentColor" opacity="0.7"/><rect x="42" y="10" width="8" height="10" rx="1" fill="currentColor" opacity="0.4"/></svg>',
dominant: '<svg viewBox="0 0 60 24" width="60" height="24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="20" cy="12" r="4" opacity="0.2" fill="currentColor"/><circle cx="30" cy="12" r="8" fill="currentColor" opacity="0.6"/><circle cx="42" cy="12" r="3" opacity="0.15" fill="currentColor"/></svg>',
};
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;

View File

@@ -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",

View File

@@ -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": "Добавить условие",

View File

@@ -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": "添加条件",

View File

@@ -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.