From 60485bc06af7ef2919ef6282ca9e7185ca1987a6 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Tue, 21 Apr 2026 00:44:38 +1200 Subject: [PATCH] feat(ui): add clear device presets action Made-with: Cursor --- led-driver | 2 +- src/static/presets.js | 70 ++++++++++++++++++++++++++++++++-------- src/templates/index.html | 1 + 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/led-driver b/led-driver index a22702d..428ed8b 160000 --- a/led-driver +++ b/led-driver @@ -1 +1 @@ -Subproject commit a22702df4dd1e201f013b52ad699c148e5a5c9d8 +Subproject commit 428ed8b8849e44123da48fff5644fa65d6cc6953 diff --git a/src/static/presets.js b/src/static/presets.js index 9869ec0..c5ee2f4 100644 --- a/src/static/presets.js +++ b/src/static/presets.js @@ -214,6 +214,7 @@ document.addEventListener('DOMContentLoaded', () => { const presetsCloseButton = document.getElementById('presets-close-btn'); const presetsList = document.getElementById('presets-list'); const presetsAddButton = document.getElementById('preset-add-btn'); + const presetClearDeviceButton = document.getElementById('preset-clear-device-btn'); const presetEditorModal = document.getElementById('preset-editor-modal'); const presetEditorCloseButton = document.getElementById('preset-editor-close-btn'); const presetNameInput = document.getElementById('preset-name-input'); @@ -283,7 +284,8 @@ document.addEventListener('DOMContentLoaded', () => { if (!input) { return 0; } - return parseInt(input.value, 10) || 0; + const n = parseInt(String(input.value).trim(), 10); + return Number.isFinite(n) ? n : 0; }; const renderPresetColors = (colors, paletteRefs) => { @@ -564,14 +566,18 @@ document.addEventListener('DOMContentLoaded', () => { const nKey = `n${i}`; const inputEl = document.getElementById(`preset-${nKey}-input`); if (inputEl) { - if (preset[nKey] !== undefined) { - inputEl.value = preset[nKey] || 0; + if (preset[nKey] !== undefined && preset[nKey] !== null) { + const raw = preset[nKey]; + const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10); + inputEl.value = String(Number.isFinite(n) ? n : 0); } else { const label = nToLabel[nKey]; - if (label && preset[label] !== undefined) { - inputEl.value = preset[label] || 0; + if (label && preset[label] !== undefined && preset[label] !== null) { + const rawL = preset[label]; + const nL = typeof rawL === 'number' ? rawL : parseInt(String(rawL), 10); + inputEl.value = String(Number.isFinite(nL) ? nL : 0); } else { - inputEl.value = 0; + inputEl.value = '0'; } } } @@ -811,6 +817,10 @@ document.addEventListener('DOMContentLoaded', () => { }); } + const hasPatternMeta = + patternConfig && typeof patternConfig === 'object' && Object.keys(patternConfig).length > 0; + const hasAnyNLabel = visibleNKeys.size > 0; + for (let i = 1; i <= 8; i++) { const nKey = `n${i}`; const labelEl = document.getElementById(`preset-${nKey}-label`); @@ -824,7 +834,9 @@ document.addEventListener('DOMContentLoaded', () => { if (groupEl) { groupEl.style.display = show ? '' : 'none'; } - if (inputEl && !show) { + // Only clear hidden n inputs when we know this pattern's metadata (avoids wiping n3..n4 + // while definitions are still loading, or when twinkle exists only as a driver file). + if (inputEl && !show && (hasAnyNLabel || hasPatternMeta)) { inputEl.value = '0'; } } @@ -969,6 +981,30 @@ document.addEventListener('DOMContentLoaded', () => { openEditor(); }); } + if (presetClearDeviceButton) { + presetClearDeviceButton.addEventListener('click', async () => { + const section = document.querySelector('.presets-section[data-zone-id]'); + const deviceNames = tabDeviceNamesFromSection(section); + if (!deviceNames.length) { + alert('No devices found in the current zone.'); + return; + } + if (!window.confirm('Clear all presets on current zone devices?')) { + return; + } + try { + const targetMacs = + typeof window.tabsManager !== 'undefined' && + typeof window.tabsManager.resolveTabDeviceMacs === 'function' + ? await window.tabsManager.resolveTabDeviceMacs(deviceNames) + : []; + await postDriverSequence([{ v: '1', clear_presets: true, save: true }], targetMacs); + } catch (error) { + console.error('Clear device presets failed:', error); + alert('Failed to clear presets on devices.'); + } + }); + } const showAddPresetToTabModal = async (optionalTabId) => { let zoneId = optionalTabId; @@ -1405,6 +1441,14 @@ document.addEventListener('DOMContentLoaded', () => { clearForm(); }); +const coercePresetInt = (v, def = 0) => { + if (typeof v === 'number' && Number.isFinite(v)) { + return v; + } + const t = parseInt(String(v), 10); + return Number.isFinite(t) ? t : def; +}; + // Build driver messages for a single preset; deliver via /presets/push (ESP-NOW + TCP). // Send order: // 1) preset payload (optionally with save) @@ -1429,12 +1473,12 @@ const sendPresetViaEspNow = async (presetId, preset, deviceNames, saveToDevice = ? preset.brightness : (typeof preset.br === 'number' ? preset.br : 127), auto: typeof preset.auto === 'boolean' ? preset.auto : true, - n1: typeof preset.n1 === 'number' ? preset.n1 : 0, - n2: typeof preset.n2 === 'number' ? preset.n2 : 0, - n3: typeof preset.n3 === 'number' ? preset.n3 : 0, - n4: typeof preset.n4 === 'number' ? preset.n4 : 0, - n5: typeof preset.n5 === 'number' ? preset.n5 : 0, - n6: typeof preset.n6 === 'number' ? preset.n6 : 0, + n1: coercePresetInt(preset.n1), + n2: coercePresetInt(preset.n2), + n3: coercePresetInt(preset.n3), + n4: coercePresetInt(preset.n4), + n5: coercePresetInt(preset.n5), + n6: coercePresetInt(preset.n6), }, }, }; diff --git a/src/templates/index.html b/src/templates/index.html index 9ed0ae2..b39d59a 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -160,6 +160,7 @@

Presets