fix(ui): update preset send/default behavior in edit mode
This commit is contained in:
@@ -132,7 +132,7 @@ const sendEspnowMessage = (obj) => {
|
|||||||
|
|
||||||
// Send a select message for a preset to all device names in the current tab.
|
// Send a select message for a preset to all device names in the current tab.
|
||||||
// Uses the preset ID as the select key.
|
// 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]');
|
const section = sectionEl || document.querySelector('.presets-section[data-tab-id]');
|
||||||
if (!section || !presetId) {
|
if (!section || !presetId) {
|
||||||
return;
|
return;
|
||||||
@@ -155,6 +155,9 @@ const sendSelectForCurrentTabDevices = (presetId, sectionEl) => {
|
|||||||
v: '1',
|
v: '1',
|
||||||
select,
|
select,
|
||||||
};
|
};
|
||||||
|
if (saveToDevice) {
|
||||||
|
message.save = true;
|
||||||
|
}
|
||||||
|
|
||||||
sendEspnowMessage(message);
|
sendEspnowMessage(message);
|
||||||
};
|
};
|
||||||
@@ -175,11 +178,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const presetDelayInput = document.getElementById('preset-delay-input');
|
const presetDelayInput = document.getElementById('preset-delay-input');
|
||||||
const presetDefaultButton = document.getElementById('preset-default-btn');
|
const presetDefaultButton = document.getElementById('preset-default-btn');
|
||||||
const presetSaveButton = document.getElementById('preset-save-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 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1001,12 +1002,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
modal.remove();
|
modal.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close on outside click
|
|
||||||
modal.addEventListener('click', (e) => {
|
|
||||||
if (e.target === modal) {
|
|
||||||
modal.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to show add preset modal:', error);
|
console.error('Failed to show add preset modal:', error);
|
||||||
alert('Failed to load presets.');
|
alert('Failed to load presets.');
|
||||||
@@ -1103,7 +1098,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (presetEditorCloseButton) {
|
if (presetEditorCloseButton) {
|
||||||
presetEditorCloseButton.addEventListener('click', closeEditor);
|
presetEditorCloseButton.addEventListener('click', closeEditor);
|
||||||
}
|
}
|
||||||
presetClearButton.addEventListener('click', clearForm);
|
|
||||||
if (presetPatternInput) {
|
if (presetPatternInput) {
|
||||||
presetPatternInput.addEventListener('change', () => {
|
presetPatternInput.addEventListener('change', () => {
|
||||||
updatePresetNLabels(presetPatternInput.value);
|
updatePresetNLabels(presetPatternInput.value);
|
||||||
@@ -1179,9 +1173,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const close = () => modal.remove();
|
const close = () => modal.remove();
|
||||||
modal.querySelector('#pick-palette-close-btn').addEventListener('click', close);
|
modal.querySelector('#pick-palette-close-btn').addEventListener('click', close);
|
||||||
modal.addEventListener('click', (e) => {
|
|
||||||
if (e.target === modal) close();
|
|
||||||
});
|
|
||||||
|
|
||||||
list.addEventListener('click', (e) => {
|
list.addEventListener('click', (e) => {
|
||||||
const btn = e.target.closest('button');
|
const btn = e.target.closest('button');
|
||||||
@@ -1216,7 +1207,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const presetSendButton = document.getElementById('preset-send-btn');
|
const presetSendButton = document.getElementById('preset-send-btn');
|
||||||
|
|
||||||
if (presetSendButton) {
|
if (presetSendButton) {
|
||||||
presetSendButton.addEventListener('click', () => {
|
presetSendButton.addEventListener('click', async () => {
|
||||||
const payload = buildPresetPayload();
|
const payload = buildPresetPayload();
|
||||||
if (!payload.name) {
|
if (!payload.name) {
|
||||||
alert('Preset name is required to send.');
|
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
|
// Work out the preset ID: for existing presets use currentEditId, otherwise fall back to name
|
||||||
const presetId = currentEditId || payload.name;
|
const presetId = currentEditId || payload.name;
|
||||||
// First send/override the preset definition under its ID
|
// Try sends preset first, then select; never persist on device.
|
||||||
sendPresetViaEspNow(presetId, payload, null, true, false);
|
await sendPresetViaEspNow(presetId, payload, deviceNames, false, false);
|
||||||
// Then send a separate select-only message for this preset ID to all devices in the tab
|
|
||||||
sendSelectForCurrentTabDevices(presetId, section);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1273,33 +1262,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
throw new Error('Failed to save preset');
|
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
|
// Use saved preset from server response for sending
|
||||||
const saved = await response.json().catch(() => null);
|
const saved = await response.json().catch(() => null);
|
||||||
if (saved && typeof saved === 'object') {
|
if (saved && typeof saved === 'object') {
|
||||||
if (currentEditId) {
|
if (currentEditId) {
|
||||||
// PUT returns the preset object directly; use the existing ID
|
// 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 {
|
} else {
|
||||||
// POST returns { id: preset }
|
// POST returns { id: preset }
|
||||||
const entries = Object.entries(saved);
|
const entries = Object.entries(saved);
|
||||||
if (entries.length > 0) {
|
if (entries.length > 0) {
|
||||||
const [newId, presetData] = entries[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 {
|
} else {
|
||||||
// Fallback: send what we just built
|
// 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();
|
await loadPresets();
|
||||||
@@ -1338,40 +1320,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
openEditor();
|
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();
|
clearForm();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build an ESPNow preset message for a single preset and optionally include a select
|
// Build ESPNow messages for a single preset.
|
||||||
// for the given device names, then send it via WebSocket.
|
// Send order:
|
||||||
|
// 1) preset payload (without save)
|
||||||
|
// 2) optional select for device names
|
||||||
|
// 3) optional save command
|
||||||
// saveToDevice defaults to true.
|
// saveToDevice defaults to true.
|
||||||
const sendPresetViaEspNow = async (presetId, preset, deviceNames, saveToDevice = true, setDefault = false) => {
|
const sendPresetViaEspNow = async (presetId, preset, deviceNames, saveToDevice = true, setDefault = false) => {
|
||||||
try {
|
try {
|
||||||
@@ -1381,7 +1337,7 @@ const sendPresetViaEspNow = async (presetId, preset, deviceNames, saveToDevice =
|
|||||||
const paletteColors = await getCurrentProfilePaletteColors();
|
const paletteColors = await getCurrentProfilePaletteColors();
|
||||||
const colors = resolveColorsWithPaletteRefs(baseColors, preset.palette_refs, paletteColors);
|
const colors = resolveColorsWithPaletteRefs(baseColors, preset.palette_refs, paletteColors);
|
||||||
|
|
||||||
const message = {
|
const presetMessage = {
|
||||||
v: '1',
|
v: '1',
|
||||||
presets: {
|
presets: {
|
||||||
[presetId]: {
|
[presetId]: {
|
||||||
@@ -1402,14 +1358,16 @@ const sendPresetViaEspNow = async (presetId, preset, deviceNames, saveToDevice =
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (saveToDevice) {
|
if (saveToDevice) {
|
||||||
// Instruct led-driver to save this preset when received.
|
presetMessage.save = true;
|
||||||
message.save = true;
|
|
||||||
}
|
}
|
||||||
if (setDefault) {
|
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) {
|
if (Array.isArray(deviceNames) && deviceNames.length > 0) {
|
||||||
const select = {};
|
const select = {};
|
||||||
deviceNames.forEach((name) => {
|
deviceNames.forEach((name) => {
|
||||||
@@ -1418,11 +1376,12 @@ const sendPresetViaEspNow = async (presetId, preset, deviceNames, saveToDevice =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (Object.keys(select).length > 0) {
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to send preset via ESPNow:', error);
|
console.error('Failed to send preset via ESPNow:', error);
|
||||||
alert('Failed to send preset via ESPNow.');
|
alert('Failed to send preset via ESPNow.');
|
||||||
@@ -1434,17 +1393,14 @@ const sendDefaultPreset = (presetId, deviceNames) => {
|
|||||||
alert('Select a preset to set as default.');
|
alert('Select a preset to set as default.');
|
||||||
return;
|
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 };
|
const message = { v: '1', default: presetId };
|
||||||
if (Array.isArray(deviceNames) && deviceNames.length > 0) {
|
if (targets.length > 0) {
|
||||||
const select = {};
|
message.targets = targets;
|
||||||
deviceNames.forEach((name) => {
|
|
||||||
if (name) {
|
|
||||||
select[name] = [presetId];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (Object.keys(select).length > 0) {
|
|
||||||
message.select = select;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sendEspnowMessage(message);
|
sendEspnowMessage(message);
|
||||||
};
|
};
|
||||||
@@ -1824,6 +1780,42 @@ const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
|
|||||||
editPresetFromTab(presetId, tabId, preset);
|
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');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.type = 'button';
|
removeBtn.type = 'button';
|
||||||
removeBtn.className = 'btn btn-danger btn-small';
|
removeBtn.className = 'btn btn-danger btn-small';
|
||||||
@@ -1838,6 +1830,7 @@ const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
actions.appendChild(editBtn);
|
actions.appendChild(editBtn);
|
||||||
|
actions.appendChild(defaultBtn);
|
||||||
actions.appendChild(removeBtn);
|
actions.appendChild(removeBtn);
|
||||||
row.appendChild(actions);
|
row.appendChild(actions);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -621,21 +621,23 @@ body.preset-ui-run .edit-mode-only {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.preset-tile-actions {
|
.preset-tile-actions {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
grid-auto-rows: 1fr;
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
justify-content: center;
|
align-content: stretch;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 0.15rem 0 0.15rem 0.25rem;
|
padding: 0.15rem 0 0.15rem 0.25rem;
|
||||||
|
width: 6.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preset-tile-actions .btn {
|
.preset-tile-actions .btn {
|
||||||
flex: 1 1 0;
|
width: 100%;
|
||||||
min-height: 0;
|
min-height: 2.35rem;
|
||||||
padding: 0.15rem 0.35rem;
|
padding: 0.15rem 0.35rem;
|
||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
white-space: nowrap;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-mode-toggle--edit {
|
.ui-mode-toggle--edit {
|
||||||
|
|||||||
Reference in New Issue
Block a user