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:
@@ -4,6 +4,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const patternsCloseButton = document.getElementById('patterns-close-btn');
|
||||
const patternsList = document.getElementById('patterns-list');
|
||||
const patternAddButton = document.getElementById('pattern-add-btn');
|
||||
const patternSendAllButton = document.getElementById('pattern-send-all-btn');
|
||||
const patternEditorModal = document.getElementById('pattern-editor-modal');
|
||||
const patternEditorCloseButton = document.getElementById('pattern-editor-close-btn');
|
||||
const patternCreateBtn = document.getElementById('pattern-create-btn');
|
||||
@@ -24,6 +25,71 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const getCurrentProfileId = async () => {
|
||||
try {
|
||||
const response = await fetch('/profiles/current', { headers: { Accept: 'application/json' } });
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
const data = await response.json();
|
||||
return data && (data.id || (data.profile && data.profile.id)) ? String(data.id || data.profile.id) : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const filterPresetsForCurrentProfile = async (presetsObj) => {
|
||||
const scoped = presetsObj && typeof presetsObj === 'object' ? presetsObj : {};
|
||||
const currentProfileId = await getCurrentProfileId();
|
||||
if (!currentProfileId) {
|
||||
return scoped;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(scoped).filter(([, preset]) => {
|
||||
if (!preset || typeof preset !== 'object') return false;
|
||||
if (!('profile_id' in preset)) return true;
|
||||
return String(preset.profile_id) === String(currentProfileId);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const tabDeviceNamesFromSection = (section) => {
|
||||
if (typeof window.parseTabDeviceNames === 'function') {
|
||||
return window.parseTabDeviceNames(section);
|
||||
}
|
||||
const namesAttr = section && section.getAttribute('data-device-names');
|
||||
return namesAttr
|
||||
? namesAttr.split(',').map((n) => n.trim()).filter((n) => n.length > 0)
|
||||
: [];
|
||||
};
|
||||
|
||||
const postDriverSequence = async (sequence, targetMacs, delayS = 0.05) => {
|
||||
const body = {
|
||||
sequence,
|
||||
targets: Array.isArray(targetMacs) && targetMacs.length ? [...new Set(targetMacs)] : undefined,
|
||||
delay_s: delayS,
|
||||
};
|
||||
const res = await fetch('/presets/push', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error((err && err.error) || res.statusText || 'Send failed');
|
||||
}
|
||||
return res.json().catch(() => ({}));
|
||||
};
|
||||
|
||||
const nReadableStringFromMeta = (meta, key) => {
|
||||
if (!meta || typeof meta !== 'object') {
|
||||
return '';
|
||||
@@ -424,4 +490,93 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
patternsCloseButton.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
if (patternSendAllButton) {
|
||||
patternSendAllButton.addEventListener('click', async () => {
|
||||
const section = document.querySelector('.presets-section[data-zone-id]');
|
||||
const zoneId = section ? section.dataset.zoneId : null;
|
||||
if (!zoneId) {
|
||||
alert('Could not determine current zone.');
|
||||
return;
|
||||
}
|
||||
const deviceNames = tabDeviceNamesFromSection(section);
|
||||
if (!deviceNames.length) {
|
||||
alert('No devices found in the current zone.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const [zoneRes, presetsRes] = await Promise.all([
|
||||
fetch(`/zones/${zoneId}`, { headers: { Accept: 'application/json' } }),
|
||||
fetch('/presets', { headers: { Accept: 'application/json' } }),
|
||||
]);
|
||||
if (!zoneRes.ok || !presetsRes.ok) {
|
||||
throw new Error('Failed to load zone presets');
|
||||
}
|
||||
const zoneData = await zoneRes.json();
|
||||
const allPresetsRaw = await presetsRes.json();
|
||||
const allPresets = await filterPresetsForCurrentProfile(allPresetsRaw);
|
||||
const zonePresetIds = Array.isArray(zoneData.presets_flat)
|
||||
? zoneData.presets_flat.map((id) => String(id))
|
||||
: [];
|
||||
if (!zonePresetIds.length) {
|
||||
alert('No presets found in this zone.');
|
||||
return;
|
||||
}
|
||||
|
||||
const wirePresets = {};
|
||||
zonePresetIds.forEach((presetId) => {
|
||||
const preset = allPresets[presetId];
|
||||
if (!preset) {
|
||||
return;
|
||||
}
|
||||
const colors = Array.isArray(preset.colors) && preset.colors.length
|
||||
? preset.colors
|
||||
: ['#FFFFFF'];
|
||||
wirePresets[presetId] = {
|
||||
pattern: preset.pattern || 'off',
|
||||
colors,
|
||||
delay: typeof preset.delay === 'number' ? preset.delay : 100,
|
||||
brightness: typeof preset.brightness === 'number'
|
||||
? preset.brightness
|
||||
: (typeof preset.br === 'number' ? preset.br : 127),
|
||||
auto: typeof preset.auto === 'boolean' ? preset.auto : true,
|
||||
n1: coercePresetInt(preset.n1),
|
||||
n2: coercePresetInt(preset.n2),
|
||||
n3: coercePresetInt(preset.n3),
|
||||
n4: coercePresetInt(preset.n4),
|
||||
n5: coercePresetInt(preset.n5),
|
||||
n6: coercePresetInt(preset.n6),
|
||||
};
|
||||
});
|
||||
if (!Object.keys(wirePresets).length) {
|
||||
alert('No matching presets found to send.');
|
||||
return;
|
||||
}
|
||||
|
||||
const select = {};
|
||||
deviceNames.forEach((name) => {
|
||||
if (name) {
|
||||
select[name] = zonePresetIds.slice();
|
||||
}
|
||||
});
|
||||
const targetMacs =
|
||||
typeof window.tabsManager !== 'undefined' &&
|
||||
typeof window.tabsManager.resolveTabDeviceMacs === 'function'
|
||||
? await window.tabsManager.resolveTabDeviceMacs(deviceNames)
|
||||
: [];
|
||||
|
||||
const sequence = [
|
||||
{ v: '1', clear_presets: true, save: true },
|
||||
{ v: '1', presets: wirePresets, save: true },
|
||||
];
|
||||
if (Object.keys(select).length) {
|
||||
sequence.push({ v: '1', select });
|
||||
}
|
||||
await postDriverSequence(sequence, targetMacs, 0.05);
|
||||
} catch (error) {
|
||||
console.error('Send all patterns failed:', error);
|
||||
alert('Failed to send all patterns.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user