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

@@ -1,5 +1,47 @@
// Zone management JavaScript
let currentZoneId = null;
let brightnessSendTimeout = null;
function sendZoneBrightness(value) {
const val = Math.max(0, Math.min(255, parseInt(value, 10) || 0));
const headerSlider = document.getElementById('header-brightness-slider');
const menuSlider = document.getElementById('menu-brightness-slider');
if (headerSlider && String(headerSlider.value) !== String(val)) {
headerSlider.value = String(val);
}
if (menuSlider && String(menuSlider.value) !== String(val)) {
menuSlider.value = String(val);
}
if (brightnessSendTimeout) {
clearTimeout(brightnessSendTimeout);
}
brightnessSendTimeout = setTimeout(() => {
(async () => {
try {
const section = document.querySelector('.presets-section[data-zone-id]');
const names = typeof window.parseTabDeviceNames === 'function'
? window.parseTabDeviceNames(section)
: [];
const targetMacs =
names.length > 0 &&
typeof window.tabsManager !== 'undefined' &&
typeof window.tabsManager.resolveTabDeviceMacs === 'function'
? await window.tabsManager.resolveTabDeviceMacs(names)
: [];
if (typeof window.postDriverSequence === 'function') {
await window.postDriverSequence([{ v: '1', b: val, save: true }], targetMacs, 0);
return;
}
// Fallback to raw websocket sender if presets.js helper isn't available yet.
if (typeof window.sendEspnowRaw === 'function') {
window.sendEspnowRaw({ v: '1', b: val, save: true });
}
} catch (err) {
console.error('Failed to send brightness via driver sequence:', err);
}
})();
}, 150);
}
const isEditModeActive = () => {
const toggle = document.querySelector('.ui-mode-toggle');
@@ -468,37 +510,17 @@ async function loadZoneContent(zoneId) {
const legacyAttr = legacyOk ? ` data-device-names="${escapeHtmlAttr(names.join(","))}"` : "";
container.innerHTML = `
<div class="presets-section" data-zone-id="${zoneId}" data-device-names-json="${namesJsonAttr}"${legacyAttr}>
<div class="profiles-actions presets-toolbar" style="margin-bottom: 1rem;">
<div class="zone-brightness-group">
<label for="zone-brightness-slider">Brightness</label>
<input type="range" id="zone-brightness-slider" min="0" max="255" value="255">
</div>
</div>
<div id="presets-list-zone" class="presets-list">
<!-- Presets will be loaded here by presets.js -->
</div>
</div>
`;
// Wire up per-zone brightness slider to send global brightness via ESPNow.
const brightnessSlider = container.querySelector('#zone-brightness-slider');
let brightnessSendTimeout = null;
if (brightnessSlider) {
brightnessSlider.addEventListener('input', (e) => {
const val = parseInt(e.target.value, 10) || 0;
if (brightnessSendTimeout) {
clearTimeout(brightnessSendTimeout);
}
brightnessSendTimeout = setTimeout(() => {
if (typeof window.sendEspnowRaw === 'function') {
try {
window.sendEspnowRaw({ v: '1', b: val, save: true });
} catch (err) {
console.error('Failed to send brightness via ESPNow:', err);
}
}
}, 150);
});
// Keep header and menu brightness controls in sync.
const brightnessSlider = document.getElementById('header-brightness-slider');
const menuBrightnessSlider = document.getElementById('menu-brightness-slider');
if (menuBrightnessSlider && brightnessSlider) {
menuBrightnessSlider.value = brightnessSlider.value;
}
// Trigger presets loading if the function exists
@@ -967,6 +989,21 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
const menuBrightnessSlider = document.getElementById('menu-brightness-slider');
if (menuBrightnessSlider) {
menuBrightnessSlider.addEventListener('input', (e) => {
sendZoneBrightness(e.target.value);
});
}
const headerBrightnessSlider = document.getElementById('header-brightness-slider');
if (headerBrightnessSlider) {
headerBrightnessSlider.addEventListener('input', (e) => {
sendZoneBrightness(e.target.value);
});
// Initial sync so both controls start aligned.
sendZoneBrightness(headerBrightnessSlider.value);
}
// When run/edit mode toggles, refresh tabs UI so edit actions show/hide immediately.
document.querySelectorAll('.ui-mode-toggle').forEach((btn) => {
btn.addEventListener('click', async () => {