feat(patterns): add new pattern suite and improve mobile controls

Add a broad set of LED patterns with metadata/tests and update zone/profile preset seeding, while refining mobile/desktop UI behavior for scrolling, brightness controls, and bulk pattern sending.
This commit is contained in:
2026-04-23 20:07:55 +12:00
parent ff92451a76
commit 6cbb728d9a
11 changed files with 331 additions and 167 deletions

View File

@@ -175,39 +175,6 @@ async function postDriverSequence(sequence, targetMacs, delayS) {
return res.json().catch(() => ({}));
}
// Send a select message for a preset to all devices on the current zone (ESP-NOW or Wi-Fi).
const sendSelectForCurrentTabDevices = async (presetId, sectionEl) => {
const section = sectionEl || document.querySelector('.presets-section[data-zone-id]');
if (!section || !presetId) {
return;
}
const deviceNames = tabDeviceNamesFromSection(section);
if (!deviceNames.length) {
return;
}
const select = {};
deviceNames.forEach((name) => {
if (name) {
select[name] = [presetId];
}
});
const targetMacs =
typeof window.tabsManager !== 'undefined' &&
typeof window.tabsManager.resolveTabDeviceMacs === 'function'
? await window.tabsManager.resolveTabDeviceMacs(deviceNames)
: [];
try {
await postDriverSequence([{ v: '1', select }], targetMacs);
} catch (err) {
console.error('sendSelectForCurrentTabDevices:', err);
alert('Failed to send preset selection to devices.');
}
};
document.addEventListener('DOMContentLoaded', () => {
const presetsButton = document.getElementById('presets-btn');
const presetsModal = document.getElementById('presets-modal');
@@ -1332,7 +1299,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Work out the preset ID: for existing presets use currentEditId, otherwise fall back to name
const presetId = currentEditId || payload.name;
// Try sends preset first, then select; never persist on device.
await sendPresetViaEspNow(presetId, payload, deviceNames, false, false);
await sendPresetViaEspNow(presetId, payload, deviceNames, false, false, '2');
});
}
@@ -1346,8 +1313,9 @@ document.addEventListener('DOMContentLoaded', () => {
const section = document.querySelector('.presets-section[data-zone-id]');
const deviceNames = tabDeviceNamesFromSection(section);
const presetId = currentEditId || payload.name;
await sendPresetViaEspNow(presetId, payload, deviceNames, true, true, '1');
await updateTabDefaultPreset(presetId);
await sendDefaultPreset(presetId, deviceNames);
await sendDefaultPreset('1', deviceNames);
});
}
@@ -1379,7 +1347,7 @@ document.addEventListener('DOMContentLoaded', () => {
throw new Error('Failed to save preset');
}
// Same device targeting as Try: zone tab supplies names → /presets/push gets targets + select.
// Same device targeting as Try: zone tab supplies names and selection without persistence.
const section = document.querySelector('.presets-section[data-zone-id]');
const deviceNames = tabDeviceNamesFromSection(section);
@@ -1388,18 +1356,18 @@ document.addEventListener('DOMContentLoaded', () => {
if (saved && typeof saved === 'object') {
if (currentEditId) {
// PUT returns the preset object directly; use the existing ID
await sendPresetViaEspNow(currentEditId, saved, deviceNames, true, false);
await sendPresetViaEspNow(currentEditId, saved, deviceNames, false, false, '2');
} else {
// POST returns { id: preset }
const entries = Object.entries(saved);
if (entries.length > 0) {
const [newId, presetData] = entries[0];
await sendPresetViaEspNow(newId, presetData, deviceNames, true, false);
await sendPresetViaEspNow(newId, presetData, deviceNames, false, false, '2');
}
}
} else {
// Fallback: send what we just built
await sendPresetViaEspNow(currentEditId || payload.name, payload, deviceNames, true, false);
await sendPresetViaEspNow(currentEditId || payload.name, payload, deviceNames, false, false, '2');
}
await loadPresets();
@@ -1454,7 +1422,14 @@ const coercePresetInt = (v, def = 0) => {
// 1) preset payload (optionally with save)
// 2) optional select for device names (never with save)
// saveToDevice defaults to true.
const sendPresetViaEspNow = async (presetId, preset, deviceNames, saveToDevice = true, setDefault = false) => {
const sendPresetViaEspNow = async (
presetId,
preset,
deviceNames,
saveToDevice = true,
setDefault = false,
devicePresetId = null,
) => {
try {
const baseColors = Array.isArray(preset.colors) && preset.colors.length
? preset.colors
@@ -1462,10 +1437,11 @@ const sendPresetViaEspNow = async (presetId, preset, deviceNames, saveToDevice =
const paletteColors = await getCurrentProfilePaletteColors();
const colors = resolveColorsWithPaletteRefs(baseColors, preset.palette_refs, paletteColors);
const wirePresetId = devicePresetId != null ? String(devicePresetId) : String(presetId);
const presetMessage = {
v: '1',
presets: {
[presetId]: {
[wirePresetId]: {
pattern: preset.pattern || 'off',
colors,
delay: typeof preset.delay === 'number' ? preset.delay : 100,
@@ -1486,7 +1462,7 @@ const sendPresetViaEspNow = async (presetId, preset, deviceNames, saveToDevice =
presetMessage.save = true;
}
if (setDefault) {
presetMessage.default = presetId;
presetMessage.default = wirePresetId;
}
const names = Array.isArray(deviceNames) ? deviceNames : [];
@@ -1502,7 +1478,7 @@ const sendPresetViaEspNow = async (presetId, preset, deviceNames, saveToDevice =
const select = {};
names.forEach((name) => {
if (name) {
select[name] = [presetId];
select[name] = [wirePresetId];
}
});
if (Object.keys(select).length > 0) {
@@ -1879,7 +1855,8 @@ const createPresetButton = (presetId, preset, zoneId, isSelected = false) => {
button.classList.add('active');
selectedPresets[zoneId] = presetId;
const section = row.closest('.presets-section');
sendSelectForCurrentTabDevices(presetId, section).catch((err) => {
const deviceNames = tabDeviceNamesFromSection(section);
sendPresetViaEspNow(presetId, preset, deviceNames, false, false, '2').catch((err) => {
console.error(err);
});
});