feat(ui): pattern modes, bundles, and zone content kind
Add profile/preset/sequence JSON import and export; map preset mode to wire n6 with a mode dropdown for multi-mode patterns; zone edit shows presets or sequences only with content_kind on save; update catalogue and tests for merged pattern names. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,25 @@ let espnowSocketReady = false;
|
||||
let espnowPendingMessages = [];
|
||||
let currentProfileIdCache = null;
|
||||
|
||||
function 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;
|
||||
}
|
||||
|
||||
/** Style variant for wire ``n6``; presets may store ``mode`` or legacy ``n6``. */
|
||||
function presetWireN6(preset, def = 0) {
|
||||
if (!preset || typeof preset !== 'object') {
|
||||
return def;
|
||||
}
|
||||
if (preset.mode !== undefined && preset.mode !== null && preset.mode !== '') {
|
||||
return coercePresetInt(preset.mode, def);
|
||||
}
|
||||
return coercePresetInt(preset.n6, def);
|
||||
}
|
||||
|
||||
const getCurrentProfileId = async () => {
|
||||
try {
|
||||
const res = await fetch('/profiles/current', { headers: { Accept: 'application/json' } });
|
||||
@@ -243,6 +262,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const presetSaveButton = document.getElementById('preset-save-btn');
|
||||
const presetAddFromPaletteButton = document.getElementById('preset-add-from-palette-btn');
|
||||
const presetBackgroundFromPaletteButton = document.getElementById('preset-background-from-palette-btn');
|
||||
const presetModeInput = document.getElementById('preset-mode-input');
|
||||
const presetModeGroup = document.getElementById('preset-mode-group');
|
||||
|
||||
if (!presetsButton || !presetsModal || !presetsList || !presetSaveButton) {
|
||||
return;
|
||||
@@ -297,7 +318,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
patternConfig.parameter_mappings &&
|
||||
typeof patternConfig.parameter_mappings === 'object'
|
||||
) {
|
||||
patternConfig = patternConfig.parameter_mappings;
|
||||
const { parameter_mappings: pm, data: _data, ...rest } = patternConfig;
|
||||
patternConfig = { ...rest, ...pm };
|
||||
}
|
||||
return patternConfig && typeof patternConfig === 'object' ? patternConfig : null;
|
||||
};
|
||||
@@ -311,6 +333,46 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return cfg.supports_manual !== false;
|
||||
};
|
||||
|
||||
const getPatternModeOptions = (patternName) => {
|
||||
const cfg = resolvePatternConfig(patternName);
|
||||
if (!cfg || typeof cfg.mode !== 'object' || cfg.mode === null || Array.isArray(cfg.mode)) {
|
||||
return null;
|
||||
}
|
||||
const entries = Object.entries(cfg.mode).filter(
|
||||
([, label]) => typeof label === 'string' && label.trim(),
|
||||
);
|
||||
if (entries.length < 2) {
|
||||
return null;
|
||||
}
|
||||
entries.sort((a, b) => parseInt(a[0], 10) - parseInt(b[0], 10));
|
||||
return entries;
|
||||
};
|
||||
|
||||
const patternSupportsModes = (patternName) => getPatternModeOptions(patternName) !== null;
|
||||
|
||||
const setPresetModeFieldVisible = (show) => {
|
||||
if (!presetModeGroup) {
|
||||
return;
|
||||
}
|
||||
presetModeGroup.hidden = !show;
|
||||
presetModeGroup.style.display = show ? '' : 'none';
|
||||
if (!show && presetModeInput) {
|
||||
presetModeInput.innerHTML = '';
|
||||
}
|
||||
};
|
||||
|
||||
const presetStoredMode = (preset) => {
|
||||
if (!preset || typeof preset !== 'object') {
|
||||
return 0;
|
||||
}
|
||||
if (preset.mode !== undefined && preset.mode !== null && preset.mode !== '') {
|
||||
const m = parseInt(String(preset.mode), 10);
|
||||
return Number.isFinite(m) ? m : 0;
|
||||
}
|
||||
const n6 = parseInt(String(preset.n6), 10);
|
||||
return Number.isFinite(n6) ? n6 : 0;
|
||||
};
|
||||
|
||||
const updateManualBeatNVisibility = () => {
|
||||
if (!presetManualBeatNWrap) {
|
||||
return;
|
||||
@@ -734,7 +796,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
// After values: show only mapped n params with labels from pattern.json; clear hidden inputs
|
||||
updatePresetNLabels(patternName);
|
||||
updatePresetNLabels(patternName, preset);
|
||||
updateManualModeAvailability();
|
||||
updatePresetEditorTabActionsVisibility();
|
||||
};
|
||||
@@ -793,10 +855,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return section ? section.dataset.zoneId : null;
|
||||
};
|
||||
|
||||
const updatePresetEditorTabActionsVisibility = () => {
|
||||
const updatePresetEditorTabActionsVisibility = async () => {
|
||||
if (!presetRemoveFromTabButton) return;
|
||||
const show = Boolean(currentEditTabId && currentEditId);
|
||||
presetRemoveFromTabButton.hidden = !show;
|
||||
if (!currentEditTabId || !currentEditId) {
|
||||
presetRemoveFromTabButton.hidden = true;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const tabRes = await fetch(`/zones/${currentEditTabId}`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!tabRes.ok) {
|
||||
presetRemoveFromTabButton.hidden = false;
|
||||
return;
|
||||
}
|
||||
const tabData = await tabRes.json();
|
||||
const allowed =
|
||||
typeof window.zoneAllowsPresets === 'function'
|
||||
? window.zoneAllowsPresets(tabData)
|
||||
: true;
|
||||
presetRemoveFromTabButton.hidden = !allowed;
|
||||
} catch (e) {
|
||||
presetRemoveFromTabButton.hidden = false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateTabDefaultPreset = async (presetId) => {
|
||||
@@ -827,8 +908,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (presetEditorModal) {
|
||||
presetEditorModal.classList.add('active');
|
||||
}
|
||||
const patternName = presetPatternInput ? presetPatternInput.value : '';
|
||||
const modeBefore = patternSupportsModes(patternName)
|
||||
? presetStoredMode({
|
||||
mode: presetModeInput ? presetModeInput.value : undefined,
|
||||
n6: getNumberInput('preset-n6-input'),
|
||||
})
|
||||
: 0;
|
||||
loadPatterns().then(() => {
|
||||
updatePresetNLabels(presetPatternInput ? presetPatternInput.value : '');
|
||||
updatePresetNLabels(patternName, { mode: modeBefore, n6: modeBefore });
|
||||
updateColorSectionVisibility();
|
||||
});
|
||||
};
|
||||
@@ -859,11 +947,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
})(),
|
||||
};
|
||||
|
||||
// Always store numeric parameters as n1..n8.
|
||||
// Always store numeric parameters as n1..n8 (except n6 when pattern uses mode).
|
||||
const modeEntries = patternSupportsModes(payload.pattern)
|
||||
? getPatternModeOptions(payload.pattern)
|
||||
: null;
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
const nKey = `n${i}`;
|
||||
if (modeEntries && nKey === 'n6') {
|
||||
continue;
|
||||
}
|
||||
payload[nKey] = getNumberInput(`preset-${nKey}-input`);
|
||||
}
|
||||
if (modeEntries && presetModeInput) {
|
||||
payload.mode = parseInt(presetModeInput.value, 10) || 0;
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
@@ -950,30 +1047,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const updatePresetNLabels = (patternName) => {
|
||||
const rawPatternName = String(patternName || '').trim();
|
||||
const normalizedPatternName = rawPatternName.endsWith('.py')
|
||||
? rawPatternName.slice(0, -3)
|
||||
: rawPatternName;
|
||||
let patternConfig =
|
||||
(cachedPatterns && cachedPatterns[rawPatternName]) ||
|
||||
(cachedPatterns && cachedPatterns[normalizedPatternName]) ||
|
||||
null;
|
||||
if (!patternConfig && cachedPatterns && typeof cachedPatterns === 'object') {
|
||||
const lower = normalizedPatternName.toLowerCase();
|
||||
const matchedKey = Object.keys(cachedPatterns).find(
|
||||
(k) => String(k).toLowerCase() === lower,
|
||||
);
|
||||
if (matchedKey) {
|
||||
patternConfig = cachedPatterns[matchedKey];
|
||||
}
|
||||
}
|
||||
if (patternConfig && typeof patternConfig === 'object' && patternConfig.data && typeof patternConfig.data === 'object') {
|
||||
patternConfig = patternConfig.data;
|
||||
}
|
||||
if (patternConfig && typeof patternConfig === 'object' && patternConfig.parameter_mappings && typeof patternConfig.parameter_mappings === 'object') {
|
||||
patternConfig = patternConfig.parameter_mappings;
|
||||
}
|
||||
const updatePresetNLabels = (patternName, presetForMode = null) => {
|
||||
const patternConfig = resolvePatternConfig(patternName);
|
||||
const labels = {};
|
||||
const visibleNKeys = new Set();
|
||||
|
||||
@@ -989,9 +1064,35 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
}
|
||||
|
||||
const modeEntries = patternSupportsModes(patternName) ? getPatternModeOptions(patternName) : null;
|
||||
if (modeEntries) {
|
||||
visibleNKeys.delete('n6');
|
||||
}
|
||||
if (presetModeInput) {
|
||||
if (modeEntries) {
|
||||
setPresetModeFieldVisible(true);
|
||||
presetModeInput.innerHTML = '';
|
||||
modeEntries.forEach(([val, label]) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = val;
|
||||
opt.textContent = label.trim();
|
||||
presetModeInput.appendChild(opt);
|
||||
});
|
||||
const modeVal = presetForMode ? presetStoredMode(presetForMode) : 0;
|
||||
const modeStr = String(modeVal);
|
||||
if ([...presetModeInput.options].some((o) => o.value === modeStr)) {
|
||||
presetModeInput.value = modeStr;
|
||||
} else if (presetModeInput.options.length) {
|
||||
presetModeInput.selectedIndex = 0;
|
||||
}
|
||||
} else {
|
||||
setPresetModeFieldVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
const hasPatternMeta =
|
||||
patternConfig && typeof patternConfig === 'object' && Object.keys(patternConfig).length > 0;
|
||||
const hasAnyNLabel = visibleNKeys.size > 0;
|
||||
const hasAnyNLabel = visibleNKeys.size > 0 || Boolean(modeEntries);
|
||||
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
const nKey = `n${i}`;
|
||||
@@ -1073,6 +1174,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
void sendPresetViaEspNow(presetId, preset || {}, []);
|
||||
});
|
||||
|
||||
const exportButton = document.createElement('button');
|
||||
exportButton.className = 'btn btn-secondary btn-small';
|
||||
exportButton.textContent = 'Export';
|
||||
exportButton.addEventListener('click', async () => {
|
||||
try {
|
||||
const response = await fetch(`/presets/${presetId}/export`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Export failed');
|
||||
}
|
||||
const bundle = await response.json();
|
||||
const safeName = ((preset && preset.name) || presetId).replace(/[^\w.-]+/g, '_');
|
||||
window.downloadJsonFile(`preset-${safeName}.json`, bundle);
|
||||
} catch (error) {
|
||||
console.error('Export preset failed:', error);
|
||||
alert('Failed to export preset.');
|
||||
}
|
||||
});
|
||||
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.className = 'btn btn-danger btn-small';
|
||||
deleteButton.textContent = 'Delete';
|
||||
@@ -1102,6 +1223,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
row.appendChild(label);
|
||||
row.appendChild(details);
|
||||
row.appendChild(editButton);
|
||||
row.appendChild(exportButton);
|
||||
row.appendChild(sendButton);
|
||||
row.appendChild(deleteButton);
|
||||
presetsList.appendChild(row);
|
||||
@@ -1148,6 +1270,34 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (presetsCloseButton) {
|
||||
presetsCloseButton.addEventListener('click', closeModal);
|
||||
}
|
||||
const importPresetBtn = document.getElementById('import-preset-btn');
|
||||
if (importPresetBtn) {
|
||||
importPresetBtn.addEventListener('click', async () => {
|
||||
const text = await window.pickJsonFile();
|
||||
if (!text) return;
|
||||
const bundle = window.parseJsonFileText(text);
|
||||
if (!bundle || bundle.kind !== 'preset') {
|
||||
alert('Invalid preset bundle file.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch('/presets/import', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ bundle }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.error || 'Import failed');
|
||||
}
|
||||
await loadPresets();
|
||||
} catch (error) {
|
||||
console.error('Import preset failed:', error);
|
||||
alert(error.message || 'Failed to import preset.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (presetsAddButton) {
|
||||
presetsAddButton.addEventListener('click', () => {
|
||||
clearForm();
|
||||
@@ -1198,6 +1348,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
alert('Could not determine current zone.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const zoneCheck = await fetch(`/zones/${zoneId}`, { headers: { Accept: 'application/json' } });
|
||||
if (zoneCheck.ok) {
|
||||
const zoneDoc = await zoneCheck.json();
|
||||
if (
|
||||
typeof window.zoneAllowsPresets === 'function' &&
|
||||
!window.zoneAllowsPresets(zoneDoc)
|
||||
) {
|
||||
alert('This zone is for sequences only. Add a sequence from the zone Edit menu instead.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not verify zone content kind:', e);
|
||||
}
|
||||
|
||||
// Load all presets
|
||||
try {
|
||||
@@ -1327,11 +1493,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
throw new Error('Failed to load zone');
|
||||
}
|
||||
const tabData = await tabResponse.json();
|
||||
const kind =
|
||||
typeof window.normalizeZoneContentKind === 'function'
|
||||
? window.normalizeZoneContentKind(tabData)
|
||||
: null;
|
||||
if (kind === 'sequences') {
|
||||
if (
|
||||
typeof window.zoneAllowsPresets === 'function' &&
|
||||
!window.zoneAllowsPresets(tabData)
|
||||
) {
|
||||
alert('This zone is for sequences only. Add a sequence from the zone Edit menu instead.');
|
||||
return;
|
||||
}
|
||||
@@ -1697,14 +1862,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
clearForm();
|
||||
});
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
/** Device field ``a`` / API ``auto``; missing → auto-run (matches server build_preset_dict). */
|
||||
const coercePresetAuto = (preset) => {
|
||||
if (!preset || typeof preset !== 'object') {
|
||||
@@ -1826,7 +1983,7 @@ const sendPresetViaEspNow = async (
|
||||
n3: coercePresetInt(preset.n3),
|
||||
n4: coercePresetInt(preset.n4),
|
||||
n5: coercePresetInt(preset.n5),
|
||||
n6: coercePresetInt(preset.n6),
|
||||
n6: presetWireN6(preset),
|
||||
manual_beat_n: coerceManualBeatN(preset),
|
||||
},
|
||||
},
|
||||
@@ -1929,7 +2086,7 @@ try {
|
||||
// window may not exist in some environments; ignore.
|
||||
}
|
||||
|
||||
// Store selected preset(s) per zone (multi-select; merge send order = click order, last wins on device).
|
||||
// Store selected preset per zone (single-select; one tile active, one driver push per click).
|
||||
const zoneSelectedPresetIds = {};
|
||||
const zonePresetSelectionOrder = {};
|
||||
|
||||
@@ -1956,19 +2113,21 @@ function getOrderedZonePresetSelection(zoneId) {
|
||||
return (zonePresetSelectionOrder[z] || []).filter((id) => set.has(String(id)));
|
||||
}
|
||||
|
||||
async function sendMergedZonePresetSelection(zoneId, tabData, allPresets) {
|
||||
const ids = getOrderedZonePresetSelection(zoneId);
|
||||
if (!ids.length) return;
|
||||
for (let i = 0; i < ids.length; i += 1) {
|
||||
const pid = ids[i];
|
||||
const preset = allPresets[pid];
|
||||
if (!preset) continue;
|
||||
const names =
|
||||
window.zonesManager && typeof window.zonesManager.resolveDeviceNamesForZonePreset === 'function'
|
||||
? await window.zonesManager.resolveDeviceNamesForZonePreset(tabData, pid)
|
||||
: [];
|
||||
await sendPresetViaEspNow(pid, preset, names, false, false, '2');
|
||||
}
|
||||
/** Preset id that should show the tile outline (last click in selection order). */
|
||||
function getLastZonePresetSelectionId(zoneId) {
|
||||
const order = getOrderedZonePresetSelection(zoneId);
|
||||
return order.length ? String(order[order.length - 1]) : null;
|
||||
}
|
||||
|
||||
async function sendZonePresetSelection(zoneId, tabData, presetId, preset, allPresets) {
|
||||
const pid = String(presetId);
|
||||
const body = (allPresets && allPresets[pid]) || preset;
|
||||
if (!body) return;
|
||||
const names =
|
||||
window.zonesManager && typeof window.zonesManager.resolveDeviceNamesForZonePreset === 'function'
|
||||
? await window.zonesManager.resolveDeviceNamesForZonePreset(tabData, pid)
|
||||
: [];
|
||||
await sendPresetViaEspNow(pid, body, names, false, false, '2');
|
||||
}
|
||||
|
||||
// Store selected preset per zone
|
||||
@@ -2053,6 +2212,12 @@ const savePresetGrid = async (zoneId, presetGrid) => {
|
||||
throw new Error('Failed to load zone');
|
||||
}
|
||||
const tabData = await tabResponse.json();
|
||||
if (
|
||||
typeof window.zoneAllowsPresets === 'function' &&
|
||||
!window.zoneAllowsPresets(tabData)
|
||||
) {
|
||||
throw new Error('This zone is for sequences only.');
|
||||
}
|
||||
|
||||
// Store as 2D grid
|
||||
tabData.presets = presetGrid;
|
||||
@@ -2265,7 +2430,9 @@ const renderTabPresets = async (zoneId, options = {}) => {
|
||||
const preset = allPresets[presetId];
|
||||
if (preset) {
|
||||
ensureZonePresetSelection(zoneId);
|
||||
const isSelected = zoneSelectedPresetIds[String(zoneId)].has(String(presetId));
|
||||
const lastSelectedId = getLastZonePresetSelectionId(zoneId);
|
||||
const isSelected =
|
||||
lastSelectedId !== null && lastSelectedId === String(presetId);
|
||||
const displayPreset = {
|
||||
...preset,
|
||||
colors: resolveColorsWithPaletteRefs(preset.colors, preset.palette_refs, paletteColors),
|
||||
@@ -2285,7 +2452,10 @@ const renderTabPresets = async (zoneId, options = {}) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof window.appendZoneSequenceTiles === 'function' && ck !== 'presets') {
|
||||
if (
|
||||
typeof window.appendZoneSequenceTiles === 'function' &&
|
||||
(typeof window.zoneAllowsSequences !== 'function' || window.zoneAllowsSequences(tabData))
|
||||
) {
|
||||
await window.appendZoneSequenceTiles(zoneId, tabData, allPresets, paletteColors, presetsList);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -2311,7 +2481,9 @@ const createPresetButton = (presetId, preset, zoneId, isSelected, tabData, group
|
||||
}
|
||||
|
||||
const colors = Array.isArray(preset.colors) ? preset.colors.filter((c) => c) : [];
|
||||
const isRainbow = (preset.pattern || '').toLowerCase() === 'rainbow';
|
||||
const pat = (preset.pattern || '').toLowerCase();
|
||||
const mode = presetWireN6(preset);
|
||||
const isRainbow = pat === 'rainbow' || (pat === 'colour_cycle' && mode === 1);
|
||||
const barColors = isRainbow
|
||||
? ['#FF0000', '#FF7F00', '#FFFF00', '#00FF00', '#0000FF', '#4B0082', '#8F00FF']
|
||||
: colors;
|
||||
@@ -2389,34 +2561,32 @@ const createPresetButton = (presetId, preset, zoneId, isSelected, tabData, group
|
||||
ensureZonePresetSelection(zoneId);
|
||||
const z = String(zoneId);
|
||||
const set = zoneSelectedPresetIds[z];
|
||||
const order = zonePresetSelectionOrder[z];
|
||||
const idStr = String(presetId);
|
||||
if (set.has(idStr)) {
|
||||
set.delete(idStr);
|
||||
zonePresetSelectionOrder[z] = order.filter((x) => String(x) !== idStr);
|
||||
} else {
|
||||
const wasSelected = set.has(idStr);
|
||||
set.clear();
|
||||
zonePresetSelectionOrder[z] = [];
|
||||
if (!wasSelected) {
|
||||
set.add(idStr);
|
||||
order.push(idStr);
|
||||
zonePresetSelectionOrder[z] = [idStr];
|
||||
}
|
||||
const outlinePresetId = getLastZonePresetSelectionId(zoneId);
|
||||
if (presetsListEl) {
|
||||
presetsListEl.querySelectorAll('.preset-tile-row:not(.sequence-tile-row)').forEach((rw) => {
|
||||
const pid = rw.dataset.presetId;
|
||||
const btnEl = rw.querySelector('.preset-tile-main');
|
||||
if (!btnEl || !pid) return;
|
||||
if (set.has(String(pid))) btnEl.classList.add('active');
|
||||
if (outlinePresetId && String(pid) === outlinePresetId) btnEl.classList.add('active');
|
||||
else btnEl.classList.remove('active');
|
||||
});
|
||||
}
|
||||
const orderList = getOrderedZonePresetSelection(zoneId);
|
||||
if (orderList.length) {
|
||||
const lastPid = orderList[orderList.length - 1];
|
||||
selectedPresets[zoneId] = lastPid;
|
||||
selectedPresetPayloads[zoneId] = (allPresets && allPresets[lastPid]) || preset;
|
||||
if (!wasSelected) {
|
||||
selectedPresets[zoneId] = idStr;
|
||||
selectedPresetPayloads[zoneId] = (allPresets && allPresets[idStr]) || preset;
|
||||
void sendZonePresetSelection(zoneId, tabData, idStr, preset, allPresets);
|
||||
} else {
|
||||
delete selectedPresets[zoneId];
|
||||
delete selectedPresetPayloads[zoneId];
|
||||
}
|
||||
void sendMergedZonePresetSelection(zoneId, tabData, allPresets);
|
||||
});
|
||||
|
||||
if (canDrag) {
|
||||
@@ -2526,6 +2696,13 @@ const removePresetFromTab = async (zoneId, presetId) => {
|
||||
throw new Error('Failed to load zone');
|
||||
}
|
||||
const tabData = await tabResponse.json();
|
||||
if (
|
||||
typeof window.zoneAllowsPresets === 'function' &&
|
||||
!window.zoneAllowsPresets(tabData)
|
||||
) {
|
||||
alert('This zone is for sequences only.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize to flat array
|
||||
let flat = [];
|
||||
|
||||
Reference in New Issue
Block a user