From 4597573ac5a57435a61505b6f350aa08a47712cd Mon Sep 17 00:00:00 2001 From: Jimmy Date: Sun, 22 Mar 2026 01:47:32 +1300 Subject: [PATCH] fix(ui): update preset send/default behavior in edit mode --- src/static/presets.js | 153 ++++++++++++++++++++---------------------- src/static/style.css | 14 ++-- 2 files changed, 81 insertions(+), 86 deletions(-) diff --git a/src/static/presets.js b/src/static/presets.js index 23f24f4..bc8fa75 100644 --- a/src/static/presets.js +++ b/src/static/presets.js @@ -132,7 +132,7 @@ const sendEspnowMessage = (obj) => { // Send a select message for a preset to all device names in the current tab. // Uses the preset ID as the select key. -const sendSelectForCurrentTabDevices = (presetId, sectionEl) => { +const sendSelectForCurrentTabDevices = (presetId, sectionEl, saveToDevice = true) => { const section = sectionEl || document.querySelector('.presets-section[data-tab-id]'); if (!section || !presetId) { return; @@ -155,6 +155,9 @@ const sendSelectForCurrentTabDevices = (presetId, sectionEl) => { v: '1', select, }; + if (saveToDevice) { + message.save = true; + } sendEspnowMessage(message); }; @@ -175,11 +178,9 @@ document.addEventListener('DOMContentLoaded', () => { const presetDelayInput = document.getElementById('preset-delay-input'); const presetDefaultButton = document.getElementById('preset-default-btn'); const presetSaveButton = document.getElementById('preset-save-btn'); - const presetClearButton = document.getElementById('preset-clear-btn'); const presetAddFromPaletteButton = document.getElementById('preset-add-from-palette-btn'); - const presetRemoveFromTabButton = document.getElementById('preset-remove-from-tab-btn'); - if (!presetsButton || !presetsModal || !presetsList || !presetSaveButton || !presetClearButton) { + if (!presetsButton || !presetsModal || !presetsList || !presetSaveButton) { return; } @@ -1001,12 +1002,6 @@ document.addEventListener('DOMContentLoaded', () => { modal.remove(); }); - // Close on outside click - modal.addEventListener('click', (e) => { - if (e.target === modal) { - modal.remove(); - } - }); } catch (error) { console.error('Failed to show add preset modal:', error); alert('Failed to load presets.'); @@ -1103,7 +1098,6 @@ document.addEventListener('DOMContentLoaded', () => { if (presetEditorCloseButton) { presetEditorCloseButton.addEventListener('click', closeEditor); } - presetClearButton.addEventListener('click', clearForm); if (presetPatternInput) { presetPatternInput.addEventListener('change', () => { updatePresetNLabels(presetPatternInput.value); @@ -1179,9 +1173,6 @@ document.addEventListener('DOMContentLoaded', () => { const close = () => modal.remove(); modal.querySelector('#pick-palette-close-btn').addEventListener('click', close); - modal.addEventListener('click', (e) => { - if (e.target === modal) close(); - }); list.addEventListener('click', (e) => { const btn = e.target.closest('button'); @@ -1216,7 +1207,7 @@ document.addEventListener('DOMContentLoaded', () => { const presetSendButton = document.getElementById('preset-send-btn'); if (presetSendButton) { - presetSendButton.addEventListener('click', () => { + presetSendButton.addEventListener('click', async () => { const payload = buildPresetPayload(); if (!payload.name) { alert('Preset name is required to send.'); @@ -1230,10 +1221,8 @@ document.addEventListener('DOMContentLoaded', () => { : []; // Work out the preset ID: for existing presets use currentEditId, otherwise fall back to name const presetId = currentEditId || payload.name; - // First send/override the preset definition under its ID - sendPresetViaEspNow(presetId, payload, null, true, false); - // Then send a separate select-only message for this preset ID to all devices in the tab - sendSelectForCurrentTabDevices(presetId, section); + // Try sends preset first, then select; never persist on device. + await sendPresetViaEspNow(presetId, payload, deviceNames, false, false); }); } @@ -1273,33 +1262,26 @@ document.addEventListener('DOMContentLoaded', () => { throw new Error('Failed to save preset'); } - // Determine device names from current tab (if any) - let deviceNames = []; - const section = document.querySelector('.presets-section[data-tab-id]'); - if (section) { - const namesAttr = section.getAttribute('data-device-names'); - deviceNames = namesAttr - ? namesAttr.split(',').map((n) => n.trim()).filter((n) => n.length > 0) - : []; - } - // Use saved preset from server response for sending const saved = await response.json().catch(() => null); if (saved && typeof saved === 'object') { if (currentEditId) { // PUT returns the preset object directly; use the existing ID - sendPresetViaEspNow(currentEditId, saved, deviceNames, true, false); + // Save & Send should not force-select the preset on devices. + sendPresetViaEspNow(currentEditId, saved, [], true, false); } else { // POST returns { id: preset } const entries = Object.entries(saved); if (entries.length > 0) { const [newId, presetData] = entries[0]; - sendPresetViaEspNow(newId, presetData, deviceNames, true, false); + // Save & Send should not force-select the preset on devices. + sendPresetViaEspNow(newId, presetData, [], true, false); } } } else { // Fallback: send what we just built - sendPresetViaEspNow(payload.name, payload, deviceNames, true, false); + // Save & Send should not force-select the preset on devices. + sendPresetViaEspNow(payload.name, payload, [], true, false); } await loadPresets(); @@ -1338,40 +1320,14 @@ document.addEventListener('DOMContentLoaded', () => { openEditor(); }); - if (presetRemoveFromTabButton) { - presetRemoveFromTabButton.addEventListener('click', async () => { - if (!currentEditId) { - alert('No preset loaded to remove.'); - return; - } - try { - await removePresetFromTab(currentEditTabId, currentEditId); - closeEditor(); - } catch (e) { - // removePresetFromTab already logs and alerts on error - } - }); - } - - presetsModal.addEventListener('click', (event) => { - if (event.target === presetsModal) { - closeModal(); - } - }); - - if (presetEditorModal) { - presetEditorModal.addEventListener('click', (event) => { - if (event.target === presetEditorModal) { - closeEditor(); - } - }); - } - clearForm(); }); -// Build an ESPNow preset message for a single preset and optionally include a select -// for the given device names, then send it via WebSocket. +// Build ESPNow messages for a single preset. +// Send order: +// 1) preset payload (without save) +// 2) optional select for device names +// 3) optional save command // saveToDevice defaults to true. const sendPresetViaEspNow = async (presetId, preset, deviceNames, saveToDevice = true, setDefault = false) => { try { @@ -1381,7 +1337,7 @@ const sendPresetViaEspNow = async (presetId, preset, deviceNames, saveToDevice = const paletteColors = await getCurrentProfilePaletteColors(); const colors = resolveColorsWithPaletteRefs(baseColors, preset.palette_refs, paletteColors); - const message = { + const presetMessage = { v: '1', presets: { [presetId]: { @@ -1402,14 +1358,16 @@ const sendPresetViaEspNow = async (presetId, preset, deviceNames, saveToDevice = }, }; if (saveToDevice) { - // Instruct led-driver to save this preset when received. - message.save = true; + presetMessage.save = true; } if (setDefault) { - message.default = presetId; + presetMessage.default = presetId; } - // Optionally include a select section for specific devices + // 1) Send presets first, without save. + sendEspnowMessage(presetMessage); + + // Optionally send a separate select message for specific devices. if (Array.isArray(deviceNames) && deviceNames.length > 0) { const select = {}; deviceNames.forEach((name) => { @@ -1418,11 +1376,12 @@ const sendPresetViaEspNow = async (presetId, preset, deviceNames, saveToDevice = } }); if (Object.keys(select).length > 0) { - message.select = select; + // Small gap helps slower receivers process preset update before select. + await new Promise((resolve) => setTimeout(resolve, 30)); + sendEspnowMessage({ v: '1', select }); } } - sendEspnowMessage(message); } catch (error) { console.error('Failed to send preset via ESPNow:', error); alert('Failed to send preset via ESPNow.'); @@ -1434,17 +1393,14 @@ const sendDefaultPreset = (presetId, deviceNames) => { alert('Select a preset to set as default.'); return; } + // Default should only set startup preset, not trigger live selection. + // When device names are provided, scope the default update to those devices. + const targets = Array.isArray(deviceNames) + ? deviceNames.map((n) => (n || '').trim()).filter((n) => n.length > 0) + : []; const message = { v: '1', default: presetId }; - if (Array.isArray(deviceNames) && deviceNames.length > 0) { - const select = {}; - deviceNames.forEach((name) => { - if (name) { - select[name] = [presetId]; - } - }); - if (Object.keys(select).length > 0) { - message.select = select; - } + if (targets.length > 0) { + message.targets = targets; } sendEspnowMessage(message); }; @@ -1824,6 +1780,42 @@ const createPresetButton = (presetId, preset, tabId, isSelected = false) => { editPresetFromTab(presetId, tabId, preset); }); + const defaultBtn = document.createElement('button'); + defaultBtn.type = 'button'; + defaultBtn.className = 'btn btn-secondary btn-small'; + defaultBtn.textContent = 'Default'; + defaultBtn.title = 'Set as default preset'; + defaultBtn.addEventListener('click', async (e) => { + e.preventDefault(); + e.stopPropagation(); + if (isDraggingPreset) return; + const section = row.closest('.presets-section'); + const namesAttr = section && section.getAttribute('data-device-names'); + const deviceNames = namesAttr + ? namesAttr.split(',').map((n) => n.trim()).filter((n) => n.length > 0) + : []; + sendDefaultPreset(presetId, deviceNames); + // Persist tab-level default if we know the tab from this tile. + if (tabId) { + try { + const tabResponse = await fetch(`/tabs/${tabId}`, { + headers: { Accept: 'application/json' }, + }); + if (tabResponse.ok) { + const tabData = await tabResponse.json(); + tabData.default_preset = presetId; + await fetch(`/tabs/${tabId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(tabData), + }); + } + } catch (error) { + console.warn('Failed to save tab default preset:', error); + } + } + }); + const removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.className = 'btn btn-danger btn-small'; @@ -1838,6 +1830,7 @@ const createPresetButton = (presetId, preset, tabId, isSelected = false) => { }); actions.appendChild(editBtn); + actions.appendChild(defaultBtn); actions.appendChild(removeBtn); row.appendChild(actions); } diff --git a/src/static/style.css b/src/static/style.css index 10a69c4..c639da5 100644 --- a/src/static/style.css +++ b/src/static/style.css @@ -621,21 +621,23 @@ body.preset-ui-run .edit-mode-only { } .preset-tile-actions { - display: flex; - flex-direction: column; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-auto-rows: 1fr; gap: 0.2rem; - justify-content: center; + align-content: stretch; flex-shrink: 0; padding: 0.15rem 0 0.15rem 0.25rem; + width: 6.5rem; } .preset-tile-actions .btn { - flex: 1 1 0; - min-height: 0; + width: 100%; + min-height: 2.35rem; padding: 0.15rem 0.35rem; font-size: 0.68rem; line-height: 1.15; - white-space: nowrap; + white-space: normal; } .ui-mode-toggle--edit {