|
|
|
|
@@ -2,6 +2,72 @@
|
|
|
|
|
let espnowSocket = null;
|
|
|
|
|
let espnowSocketReady = false;
|
|
|
|
|
let espnowPendingMessages = [];
|
|
|
|
|
let currentProfileIdCache = null;
|
|
|
|
|
|
|
|
|
|
const getCurrentProfileId = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/profiles/current', { headers: { Accept: 'application/json' } });
|
|
|
|
|
if (!res.ok) return currentProfileIdCache ? String(currentProfileIdCache) : null;
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
const id = data && (data.id || (data.profile && data.profile.id));
|
|
|
|
|
currentProfileIdCache = id ? String(id) : null;
|
|
|
|
|
return currentProfileIdCache;
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return currentProfileIdCache ? String(currentProfileIdCache) : 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; // Legacy records
|
|
|
|
|
return String(preset.profile_id) === String(currentProfileId);
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getCurrentProfileData = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/profiles/current', { headers: { Accept: 'application/json' } });
|
|
|
|
|
if (!res.ok) return null;
|
|
|
|
|
return await res.json();
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getCurrentProfilePaletteColors = async () => {
|
|
|
|
|
const profileData = await getCurrentProfileData();
|
|
|
|
|
const profile = profileData && profileData.profile;
|
|
|
|
|
const paletteId = profile && (profile.palette_id || profile.paletteId);
|
|
|
|
|
if (!paletteId) return [];
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/palettes/${paletteId}`, { headers: { Accept: 'application/json' } });
|
|
|
|
|
if (!res.ok) return [];
|
|
|
|
|
const pal = await res.json();
|
|
|
|
|
return Array.isArray(pal.colors) ? pal.colors : [];
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resolveColorsWithPaletteRefs = (colors, paletteRefs, paletteColors) => {
|
|
|
|
|
const baseColors = Array.isArray(colors) ? colors : [];
|
|
|
|
|
const refs = Array.isArray(paletteRefs) ? paletteRefs : [];
|
|
|
|
|
const pal = Array.isArray(paletteColors) ? paletteColors : [];
|
|
|
|
|
return baseColors.map((color, idx) => {
|
|
|
|
|
const refRaw = refs[idx];
|
|
|
|
|
const ref = Number.isInteger(refRaw) ? refRaw : parseInt(refRaw, 10);
|
|
|
|
|
if (Number.isInteger(ref) && ref >= 0 && ref < pal.length && pal[ref]) {
|
|
|
|
|
return pal[ref];
|
|
|
|
|
}
|
|
|
|
|
return color;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getEspnowSocket = () => {
|
|
|
|
|
if (espnowSocket && (espnowSocket.readyState === WebSocket.OPEN || espnowSocket.readyState === WebSocket.CONNECTING)) {
|
|
|
|
|
@@ -105,7 +171,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
const presetPatternInput = document.getElementById('preset-pattern-input');
|
|
|
|
|
const presetColorsContainer = document.getElementById('preset-colors-container');
|
|
|
|
|
const presetNewColorInput = document.getElementById('preset-new-color');
|
|
|
|
|
const presetAddColorButton = document.getElementById('preset-add-color-btn');
|
|
|
|
|
const presetBrightnessInput = document.getElementById('preset-brightness-input');
|
|
|
|
|
const presetDelayInput = document.getElementById('preset-delay-input');
|
|
|
|
|
const presetDefaultButton = document.getElementById('preset-default-btn');
|
|
|
|
|
@@ -123,6 +188,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
let cachedPresets = {};
|
|
|
|
|
let cachedPatterns = {};
|
|
|
|
|
let currentPresetColors = []; // Track colors for the current preset
|
|
|
|
|
let currentPresetPaletteRefs = []; // Palette index refs per color (null for direct colors)
|
|
|
|
|
|
|
|
|
|
// Function to get max colors for current pattern
|
|
|
|
|
const getMaxColors = () => {
|
|
|
|
|
@@ -158,7 +224,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
|
|
|
|
|
// Hide/show the actions (color picker and buttons)
|
|
|
|
|
const colorActions = presetColorsContainer.nextElementSibling;
|
|
|
|
|
if (colorActions && (colorActions.querySelector('#preset-add-color-btn') || colorActions.querySelector('#preset-new-color'))) {
|
|
|
|
|
if (colorActions && colorActions.querySelector('#preset-new-color')) {
|
|
|
|
|
colorActions.style.display = shouldShow ? '' : 'none';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -172,11 +238,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
return parseInt(input.value, 10) || 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderPresetColors = (colors) => {
|
|
|
|
|
const renderPresetColors = (colors, paletteRefs) => {
|
|
|
|
|
if (!presetColorsContainer) return;
|
|
|
|
|
|
|
|
|
|
presetColorsContainer.innerHTML = '';
|
|
|
|
|
currentPresetColors = colors || [];
|
|
|
|
|
currentPresetColors = Array.isArray(colors) ? colors.slice() : [];
|
|
|
|
|
if (Array.isArray(paletteRefs)) {
|
|
|
|
|
currentPresetPaletteRefs = currentPresetColors.map((_, i) => {
|
|
|
|
|
const refRaw = paletteRefs[i];
|
|
|
|
|
const ref = Number.isInteger(refRaw) ? refRaw : parseInt(refRaw, 10);
|
|
|
|
|
return Number.isInteger(ref) ? ref : null;
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
currentPresetPaletteRefs = currentPresetColors.map((_, i) => {
|
|
|
|
|
const refRaw = currentPresetPaletteRefs[i];
|
|
|
|
|
const ref = Number.isInteger(refRaw) ? refRaw : parseInt(refRaw, 10);
|
|
|
|
|
return Number.isInteger(ref) ? ref : null;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get max colors for current pattern
|
|
|
|
|
const maxColors = getMaxColors();
|
|
|
|
|
@@ -185,7 +264,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
if (currentPresetColors.length === 0) {
|
|
|
|
|
const empty = document.createElement('p');
|
|
|
|
|
empty.className = 'muted-text';
|
|
|
|
|
empty.textContent = `No colors added. Click "Add Color" to add colors.${maxColorsText}`;
|
|
|
|
|
empty.textContent = `No colors added. Use the color picker to add colors.${maxColorsText}`;
|
|
|
|
|
presetColorsContainer.appendChild(empty);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@@ -208,6 +287,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
swatchWrapper.style.cssText = 'position: relative; display: inline-block;';
|
|
|
|
|
swatchWrapper.draggable = true;
|
|
|
|
|
swatchWrapper.dataset.colorIndex = index;
|
|
|
|
|
const refAtIndex = currentPresetPaletteRefs[index];
|
|
|
|
|
swatchWrapper.dataset.paletteRef = Number.isInteger(refAtIndex) ? String(refAtIndex) : '';
|
|
|
|
|
swatchWrapper.classList.add('draggable-color-swatch');
|
|
|
|
|
|
|
|
|
|
const swatch = document.createElement('div');
|
|
|
|
|
@@ -222,6 +303,31 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
transition: opacity 0.2s, transform 0.2s;
|
|
|
|
|
`;
|
|
|
|
|
swatch.title = `${color} - Drag to reorder`;
|
|
|
|
|
|
|
|
|
|
if (Number.isInteger(refAtIndex)) {
|
|
|
|
|
const linkedBadge = document.createElement('span');
|
|
|
|
|
linkedBadge.textContent = 'P';
|
|
|
|
|
linkedBadge.title = `Linked to palette color #${refAtIndex + 1}`;
|
|
|
|
|
linkedBadge.style.cssText = `
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: -6px;
|
|
|
|
|
top: -6px;
|
|
|
|
|
min-width: 18px;
|
|
|
|
|
height: 18px;
|
|
|
|
|
border-radius: 9px;
|
|
|
|
|
background: #3f51b5;
|
|
|
|
|
color: #fff;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
z-index: 11;
|
|
|
|
|
border: 1px solid rgba(255,255,255,0.35);
|
|
|
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.35);
|
|
|
|
|
`;
|
|
|
|
|
swatchWrapper.appendChild(linkedBadge);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Color picker overlay
|
|
|
|
|
const colorPicker = document.createElement('input');
|
|
|
|
|
@@ -239,7 +345,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
`;
|
|
|
|
|
colorPicker.addEventListener('change', (e) => {
|
|
|
|
|
currentPresetColors[index] = e.target.value;
|
|
|
|
|
renderPresetColors(currentPresetColors);
|
|
|
|
|
// Manual picker edit breaks palette linkage for this slot.
|
|
|
|
|
currentPresetPaletteRefs[index] = null;
|
|
|
|
|
renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
|
|
|
|
|
});
|
|
|
|
|
// Prevent color picker from interfering with drag
|
|
|
|
|
colorPicker.addEventListener('mousedown', (e) => {
|
|
|
|
|
@@ -271,7 +379,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
removeBtn.addEventListener('click', (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
currentPresetColors.splice(index, 1);
|
|
|
|
|
renderPresetColors(currentPresetColors);
|
|
|
|
|
currentPresetPaletteRefs.splice(index, 1);
|
|
|
|
|
renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
|
|
|
|
|
});
|
|
|
|
|
// Prevent remove button from interfering with drag
|
|
|
|
|
removeBtn.addEventListener('mousedown', (e) => {
|
|
|
|
|
@@ -326,12 +435,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
const colorPicker = el.querySelector('input[type="color"]');
|
|
|
|
|
return colorPicker ? colorPicker.value : null;
|
|
|
|
|
}).filter(color => color !== null);
|
|
|
|
|
const newRefOrder = colorElements.map((el) => {
|
|
|
|
|
const refRaw = el.dataset.paletteRef;
|
|
|
|
|
const ref = Number.isInteger(refRaw) ? refRaw : parseInt(refRaw, 10);
|
|
|
|
|
return Number.isInteger(ref) ? ref : null;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update current colors array
|
|
|
|
|
currentPresetColors = newColorOrder;
|
|
|
|
|
currentPresetPaletteRefs = newRefOrder;
|
|
|
|
|
|
|
|
|
|
// Re-render to update indices
|
|
|
|
|
renderPresetColors(currentPresetColors);
|
|
|
|
|
renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
presetColorsContainer.appendChild(swatchContainer);
|
|
|
|
|
@@ -361,7 +476,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
const patternName = preset.pattern || '';
|
|
|
|
|
presetPatternInput.value = patternName;
|
|
|
|
|
const colors = Array.isArray(preset.colors) ? preset.colors : [];
|
|
|
|
|
renderPresetColors(colors);
|
|
|
|
|
const paletteRefs = Array.isArray(preset.palette_refs) ? preset.palette_refs : [];
|
|
|
|
|
renderPresetColors(colors, paletteRefs);
|
|
|
|
|
presetBrightnessInput.value = preset.brightness || 0;
|
|
|
|
|
presetDelayInput.value = preset.delay || 0;
|
|
|
|
|
|
|
|
|
|
@@ -424,6 +540,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
currentEditId = null;
|
|
|
|
|
currentEditTabId = null;
|
|
|
|
|
currentPresetColors = [];
|
|
|
|
|
currentPresetPaletteRefs = [];
|
|
|
|
|
setFormValues({
|
|
|
|
|
name: '',
|
|
|
|
|
pattern: '',
|
|
|
|
|
@@ -505,6 +622,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
name: presetNameInput ? presetNameInput.value.trim() : '',
|
|
|
|
|
pattern: presetPatternInput ? presetPatternInput.value.trim() : '',
|
|
|
|
|
colors: currentPresetColors || [],
|
|
|
|
|
palette_refs: currentPresetPaletteRefs || [],
|
|
|
|
|
// Use canonical field names expected by the device / API
|
|
|
|
|
brightness: presetBrightnessInput ? parseInt(presetBrightnessInput.value, 10) || 0 : 0,
|
|
|
|
|
delay: presetDelayInput ? parseInt(presetDelayInput.value, 10) || 0 : 0,
|
|
|
|
|
@@ -633,9 +751,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
const editButton = document.createElement('button');
|
|
|
|
|
editButton.className = 'btn btn-secondary btn-small';
|
|
|
|
|
editButton.textContent = 'Edit';
|
|
|
|
|
editButton.addEventListener('click', () => {
|
|
|
|
|
editButton.addEventListener('click', async () => {
|
|
|
|
|
currentEditId = presetId;
|
|
|
|
|
setFormValues(preset || {});
|
|
|
|
|
const paletteColors = await getCurrentProfilePaletteColors();
|
|
|
|
|
const presetForEditor = {
|
|
|
|
|
...(preset || {}),
|
|
|
|
|
colors: resolveColorsWithPaletteRefs(
|
|
|
|
|
(preset && preset.colors) || [],
|
|
|
|
|
(preset && preset.palette_refs) || [],
|
|
|
|
|
paletteColors,
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
setFormValues(presetForEditor);
|
|
|
|
|
openEditor();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -698,7 +825,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
throw new Error('Failed to load presets');
|
|
|
|
|
}
|
|
|
|
|
const presets = await response.json();
|
|
|
|
|
renderPresets(presets);
|
|
|
|
|
const filtered = await filterPresetsForCurrentProfile(presets);
|
|
|
|
|
renderPresets(filtered);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Load presets failed:', error);
|
|
|
|
|
presetsList.innerHTML = '';
|
|
|
|
|
@@ -757,7 +885,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('Failed to load presets');
|
|
|
|
|
}
|
|
|
|
|
const allPresets = await response.json();
|
|
|
|
|
const allPresetsRaw = await response.json();
|
|
|
|
|
const allPresets = await filterPresetsForCurrentProfile(allPresetsRaw);
|
|
|
|
|
|
|
|
|
|
// Load only the current tab's presets so we can avoid duplicates within this tab.
|
|
|
|
|
let currentTabPresets = [];
|
|
|
|
|
@@ -946,12 +1075,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
// Update color section visibility
|
|
|
|
|
updateColorSectionVisibility();
|
|
|
|
|
// Re-render colors to show updated max colors limit
|
|
|
|
|
renderPresetColors(currentPresetColors);
|
|
|
|
|
renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// Add Color button handler
|
|
|
|
|
if (presetAddColorButton && presetNewColorInput) {
|
|
|
|
|
presetAddColorButton.addEventListener('click', () => {
|
|
|
|
|
// Color picker auto-add handler
|
|
|
|
|
if (presetNewColorInput) {
|
|
|
|
|
const tryAddSelectedColor = () => {
|
|
|
|
|
const color = presetNewColorInput.value;
|
|
|
|
|
if (!color) return;
|
|
|
|
|
|
|
|
|
|
@@ -967,60 +1096,86 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentPresetColors.push(color);
|
|
|
|
|
renderPresetColors(currentPresetColors);
|
|
|
|
|
});
|
|
|
|
|
currentPresetPaletteRefs.push(null);
|
|
|
|
|
renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
|
|
|
|
|
};
|
|
|
|
|
// Add when the picker closes (user confirms selection).
|
|
|
|
|
presetNewColorInput.addEventListener('change', tryAddSelectedColor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add from Palette button handler
|
|
|
|
|
if (presetAddFromPaletteButton) {
|
|
|
|
|
presetAddFromPaletteButton.addEventListener('click', () => {
|
|
|
|
|
const openButton = document.getElementById('color-palette-btn');
|
|
|
|
|
if (openButton) {
|
|
|
|
|
openButton.click();
|
|
|
|
|
}
|
|
|
|
|
const modal = document.getElementById('color-palette-modal');
|
|
|
|
|
const modalList = document.getElementById('palette-container');
|
|
|
|
|
if (modal) {
|
|
|
|
|
modal.classList.add('active');
|
|
|
|
|
}
|
|
|
|
|
if (!modalList) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
presetAddFromPaletteButton.addEventListener('click', async () => {
|
|
|
|
|
try {
|
|
|
|
|
const paletteColors = await getCurrentProfilePaletteColors();
|
|
|
|
|
if (!Array.isArray(paletteColors) || paletteColors.length === 0) {
|
|
|
|
|
alert('No profile palette colors available.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handlePick = (event) => {
|
|
|
|
|
const row = event.target.closest('[data-color]');
|
|
|
|
|
if (!row) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const picked = row.dataset.color;
|
|
|
|
|
if (!picked) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentPresetColors.includes(picked)) {
|
|
|
|
|
alert('This color is already in the list.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const maxColors = getMaxColors();
|
|
|
|
|
if (currentPresetColors.length >= maxColors) {
|
|
|
|
|
alert(`This pattern allows a maximum of ${maxColors} color${maxColors !== 1 ? 's' : ''}.`);
|
|
|
|
|
if (modal) {
|
|
|
|
|
modal.classList.remove('active');
|
|
|
|
|
const modal = document.createElement('div');
|
|
|
|
|
modal.className = 'modal active';
|
|
|
|
|
modal.innerHTML = `
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
<h2>Pick Palette Color</h2>
|
|
|
|
|
<div id="pick-palette-list" class="profiles-list" style="max-height: 300px; overflow-y: auto;"></div>
|
|
|
|
|
<div class="modal-actions">
|
|
|
|
|
<button class="btn btn-secondary" id="pick-palette-close-btn">Close</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
|
|
|
|
|
|
const list = modal.querySelector('#pick-palette-list');
|
|
|
|
|
paletteColors.forEach((color, idx) => {
|
|
|
|
|
const row = document.createElement('div');
|
|
|
|
|
row.className = 'profiles-row';
|
|
|
|
|
row.style.display = 'flex';
|
|
|
|
|
row.style.alignItems = 'center';
|
|
|
|
|
row.style.gap = '0.75rem';
|
|
|
|
|
row.dataset.paletteIndex = String(idx);
|
|
|
|
|
row.dataset.paletteColor = color;
|
|
|
|
|
row.innerHTML = `
|
|
|
|
|
<div style="width:28px;height:28px;border-radius:4px;border:1px solid #555;background:${color};"></div>
|
|
|
|
|
<span style="flex:1">${color}</span>
|
|
|
|
|
<button class="btn btn-primary btn-small" type="button">Use</button>
|
|
|
|
|
`;
|
|
|
|
|
list.appendChild(row);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const close = () => modal.remove();
|
|
|
|
|
modal.querySelector('#pick-palette-close-btn').addEventListener('click', close);
|
|
|
|
|
modal.addEventListener('click', (e) => {
|
|
|
|
|
if (e.target === modal) close();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
list.addEventListener('click', (e) => {
|
|
|
|
|
const btn = e.target.closest('button');
|
|
|
|
|
if (!btn) return;
|
|
|
|
|
const row = e.target.closest('[data-palette-index]');
|
|
|
|
|
if (!row) return;
|
|
|
|
|
const color = row.dataset.paletteColor;
|
|
|
|
|
const ref = parseInt(row.dataset.paletteIndex, 10);
|
|
|
|
|
if (!color || !Number.isInteger(ref)) return;
|
|
|
|
|
|
|
|
|
|
if (currentPresetColors.includes(color) && currentPresetPaletteRefs.includes(ref)) {
|
|
|
|
|
alert('That palette color is already linked.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const maxColors = getMaxColors();
|
|
|
|
|
if (currentPresetColors.length >= maxColors) {
|
|
|
|
|
alert(`This pattern allows a maximum of ${maxColors} color${maxColors !== 1 ? 's' : ''}.`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
modalList.removeEventListener('click', handlePick);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentPresetColors.push(picked);
|
|
|
|
|
renderPresetColors(currentPresetColors);
|
|
|
|
|
if (modal) {
|
|
|
|
|
modal.classList.remove('active');
|
|
|
|
|
}
|
|
|
|
|
modalList.removeEventListener('click', handlePick);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
modalList.addEventListener('click', handlePick);
|
|
|
|
|
currentPresetColors.push(color);
|
|
|
|
|
currentPresetPaletteRefs.push(ref);
|
|
|
|
|
renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
|
|
|
|
|
close();
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Failed to add from palette:', err);
|
|
|
|
|
alert('Failed to load palette colors.');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
const presetSendButton = document.getElementById('preset-send-btn');
|
|
|
|
|
@@ -1136,7 +1291,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
currentEditId = presetId;
|
|
|
|
|
currentEditTabId = tabId || null;
|
|
|
|
|
await loadPatterns();
|
|
|
|
|
setFormValues(preset);
|
|
|
|
|
const paletteColors = await getCurrentProfilePaletteColors();
|
|
|
|
|
setFormValues({
|
|
|
|
|
...(preset || {}),
|
|
|
|
|
colors: resolveColorsWithPaletteRefs(
|
|
|
|
|
(preset && preset.colors) || [],
|
|
|
|
|
(preset && preset.palette_refs) || [],
|
|
|
|
|
paletteColors,
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
openEditor();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -1175,11 +1338,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
// Build an ESPNow preset message for a single preset and optionally include a select
|
|
|
|
|
// for the given device names, then send it via WebSocket.
|
|
|
|
|
// saveToDevice defaults to true.
|
|
|
|
|
const sendPresetViaEspNow = (presetId, preset, deviceNames, saveToDevice = true, setDefault = false) => {
|
|
|
|
|
const sendPresetViaEspNow = async (presetId, preset, deviceNames, saveToDevice = true, setDefault = false) => {
|
|
|
|
|
try {
|
|
|
|
|
const colors = Array.isArray(preset.colors) && preset.colors.length
|
|
|
|
|
const baseColors = Array.isArray(preset.colors) && preset.colors.length
|
|
|
|
|
? preset.colors
|
|
|
|
|
: ['#FFFFFF'];
|
|
|
|
|
const paletteColors = await getCurrentProfilePaletteColors();
|
|
|
|
|
const colors = resolveColorsWithPaletteRefs(baseColors, preset.palette_refs, paletteColors);
|
|
|
|
|
|
|
|
|
|
const message = {
|
|
|
|
|
v: '1',
|
|
|
|
|
@@ -1261,97 +1426,29 @@ try {
|
|
|
|
|
|
|
|
|
|
// Store selected preset per tab
|
|
|
|
|
const selectedPresets = {};
|
|
|
|
|
// Run vs Edit for tab preset strip (in-memory only — each full page load starts in run mode)
|
|
|
|
|
let presetUiMode = 'run';
|
|
|
|
|
|
|
|
|
|
const getPresetUiMode = () => (presetUiMode === 'edit' ? 'edit' : 'run');
|
|
|
|
|
|
|
|
|
|
const setPresetUiMode = (mode) => {
|
|
|
|
|
presetUiMode = mode === 'edit' ? 'edit' : 'run';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateUiModeToggleButtons = () => {
|
|
|
|
|
const mode = getPresetUiMode();
|
|
|
|
|
// Label is the mode you switch *to* (opposite of current)
|
|
|
|
|
const label = mode === 'edit' ? 'Run mode' : 'Edit mode';
|
|
|
|
|
document.querySelectorAll('.ui-mode-toggle').forEach((btn) => {
|
|
|
|
|
btn.textContent = label;
|
|
|
|
|
btn.setAttribute('aria-pressed', mode === 'edit' ? 'true' : 'false');
|
|
|
|
|
btn.classList.toggle('ui-mode-toggle--edit', mode === 'edit');
|
|
|
|
|
});
|
|
|
|
|
document.body.classList.toggle('preset-ui-edit', mode === 'edit');
|
|
|
|
|
document.body.classList.toggle('preset-ui-run', mode === 'run');
|
|
|
|
|
};
|
|
|
|
|
// Track if we're currently dragging a preset
|
|
|
|
|
let isDraggingPreset = false;
|
|
|
|
|
// Context menu for tab presets
|
|
|
|
|
let presetContextMenu = null;
|
|
|
|
|
let presetContextTarget = null;
|
|
|
|
|
|
|
|
|
|
const ensurePresetContextMenu = () => {
|
|
|
|
|
if (presetContextMenu) {
|
|
|
|
|
return presetContextMenu;
|
|
|
|
|
}
|
|
|
|
|
const menu = document.createElement('div');
|
|
|
|
|
menu.id = 'preset-context-menu';
|
|
|
|
|
menu.style.cssText = `
|
|
|
|
|
position: fixed;
|
|
|
|
|
z-index: 2000;
|
|
|
|
|
background: #2e2e2e;
|
|
|
|
|
border: 1px solid #4a4a4a;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
box-shadow: 0 2px 6px rgba(0,0,0,0.6);
|
|
|
|
|
padding: 0.25rem 0;
|
|
|
|
|
min-width: 160px;
|
|
|
|
|
display: none;
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const addItem = (label, action) => {
|
|
|
|
|
const item = document.createElement('button');
|
|
|
|
|
item.type = 'button';
|
|
|
|
|
item.textContent = label;
|
|
|
|
|
item.dataset.action = action;
|
|
|
|
|
item.style.cssText = `
|
|
|
|
|
display: block;
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 0.4rem 0.75rem;
|
|
|
|
|
background: transparent;
|
|
|
|
|
color: #eee;
|
|
|
|
|
border: none;
|
|
|
|
|
text-align: left;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
`;
|
|
|
|
|
item.addEventListener('mouseover', () => {
|
|
|
|
|
item.style.backgroundColor = '#3a3a3a';
|
|
|
|
|
});
|
|
|
|
|
item.addEventListener('mouseout', () => {
|
|
|
|
|
item.style.backgroundColor = 'transparent';
|
|
|
|
|
});
|
|
|
|
|
menu.appendChild(item);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
addItem('Edit preset…', 'edit');
|
|
|
|
|
|
|
|
|
|
menu.addEventListener('click', async (e) => {
|
|
|
|
|
const btn = e.target.closest('button[data-action]');
|
|
|
|
|
if (!btn || !presetContextTarget) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const { presetId } = presetContextTarget;
|
|
|
|
|
const action = btn.dataset.action;
|
|
|
|
|
hidePresetContextMenu();
|
|
|
|
|
if (action === 'edit') {
|
|
|
|
|
await editPresetFromTab(presetId);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(menu);
|
|
|
|
|
presetContextMenu = menu;
|
|
|
|
|
|
|
|
|
|
// Hide on outside click
|
|
|
|
|
document.addEventListener('click', (e) => {
|
|
|
|
|
if (!presetContextMenu) return;
|
|
|
|
|
if (e.target.closest('#preset-context-menu')) return;
|
|
|
|
|
hidePresetContextMenu();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return menu;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const showPresetContextMenu = (x, y, tabId, presetId, preset) => {
|
|
|
|
|
const menu = ensurePresetContextMenu();
|
|
|
|
|
presetContextTarget = { tabId, presetId, preset };
|
|
|
|
|
menu.style.left = `${x}px`;
|
|
|
|
|
menu.style.top = `${y}px`;
|
|
|
|
|
menu.style.display = 'block';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const hidePresetContextMenu = () => {
|
|
|
|
|
if (presetContextMenu) {
|
|
|
|
|
presetContextMenu.style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
presetContextTarget = null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Function to convert 2D grid to flat array (for backward compatibility)
|
|
|
|
|
const gridToArray = (presetsGrid) => {
|
|
|
|
|
@@ -1449,6 +1546,25 @@ const getDropTarget = (container, x, y) => {
|
|
|
|
|
return closest.element;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Move dragged tile onto the drop target's slot.
|
|
|
|
|
* When moving down the list (fromIdx < toIdx), insertBefore(dragging, dropTarget) lands one index
|
|
|
|
|
* too early; use the next element sibling so the item occupies the target slot.
|
|
|
|
|
*/
|
|
|
|
|
const insertDraggingOntoTarget = (presetsList, dragging, dropTarget) => {
|
|
|
|
|
const siblings = [...presetsList.querySelectorAll('.draggable-preset')];
|
|
|
|
|
const fromIdx = siblings.indexOf(dragging);
|
|
|
|
|
const toIdx = siblings.indexOf(dropTarget);
|
|
|
|
|
if (fromIdx === -1 || toIdx === -1) return;
|
|
|
|
|
|
|
|
|
|
if (fromIdx < toIdx) {
|
|
|
|
|
const next = dropTarget.nextElementSibling;
|
|
|
|
|
presetsList.insertBefore(dragging, next);
|
|
|
|
|
} else {
|
|
|
|
|
presetsList.insertBefore(dragging, dropTarget);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Function to render presets for a specific tab in 2D grid
|
|
|
|
|
const renderTabPresets = async (tabId) => {
|
|
|
|
|
const presetsList = document.getElementById('presets-list-tab');
|
|
|
|
|
@@ -1482,47 +1598,74 @@ const renderTabPresets = async (tabId) => {
|
|
|
|
|
if (!presetsResponse.ok) {
|
|
|
|
|
throw new Error('Failed to load presets');
|
|
|
|
|
}
|
|
|
|
|
const allPresets = await presetsResponse.json();
|
|
|
|
|
const allPresetsRaw = await presetsResponse.json();
|
|
|
|
|
const allPresets = await filterPresetsForCurrentProfile(allPresetsRaw);
|
|
|
|
|
const paletteColors = await getCurrentProfilePaletteColors();
|
|
|
|
|
|
|
|
|
|
presetsList.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
// Add drag and drop handlers to the container
|
|
|
|
|
presetsList.addEventListener('dragover', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const dragging = presetsList.querySelector('.dragging');
|
|
|
|
|
if (!dragging) return;
|
|
|
|
|
|
|
|
|
|
const dropTarget = getDropTarget(presetsList, e.clientX, e.clientY);
|
|
|
|
|
if (dropTarget && dropTarget !== dragging) {
|
|
|
|
|
// Insert before drop target so the dragged item takes that cell's position
|
|
|
|
|
presetsList.insertBefore(dragging, dropTarget);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
presetsList.addEventListener('drop', async (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const dragging = presetsList.querySelector('.dragging');
|
|
|
|
|
if (!dragging) return;
|
|
|
|
|
|
|
|
|
|
// Get new grid layout from DOM
|
|
|
|
|
const presetElements = [...presetsList.querySelectorAll('.draggable-preset')];
|
|
|
|
|
const presetIds = presetElements.map(el => el.dataset.presetId);
|
|
|
|
|
|
|
|
|
|
// Convert to 2D grid (3 columns)
|
|
|
|
|
const newGrid = arrayToGrid(presetIds, 3);
|
|
|
|
|
|
|
|
|
|
// Save new grid
|
|
|
|
|
try {
|
|
|
|
|
await savePresetGrid(tabId, newGrid);
|
|
|
|
|
// Re-render to ensure consistency
|
|
|
|
|
await renderTabPresets(tabId);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to save preset grid:', error);
|
|
|
|
|
alert('Failed to save preset order. Please try again.');
|
|
|
|
|
// Re-render to restore original order
|
|
|
|
|
await renderTabPresets(tabId);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
presetsList.dataset.reorderTabId = tabId;
|
|
|
|
|
|
|
|
|
|
// Drag-and-drop on the list (wire once — re-render would duplicate listeners otherwise)
|
|
|
|
|
if (!presetsList.dataset.dragWired) {
|
|
|
|
|
presetsList.dataset.dragWired = '1';
|
|
|
|
|
// dragenter + dropEffect tell the browser this zone accepts a move (avoids ⊘ cursor)
|
|
|
|
|
presetsList.addEventListener('dragenter', (e) => {
|
|
|
|
|
if (getPresetUiMode() !== 'edit') return;
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
});
|
|
|
|
|
presetsList.addEventListener('dragover', (e) => {
|
|
|
|
|
if (getPresetUiMode() !== 'edit') return;
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
try {
|
|
|
|
|
e.dataTransfer.dropEffect = 'move';
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
const dragging = presetsList.querySelector('.dragging');
|
|
|
|
|
if (!dragging) return;
|
|
|
|
|
|
|
|
|
|
const dropTarget = getDropTarget(presetsList, e.clientX, e.clientY);
|
|
|
|
|
// Keep dragover side-effect free; commit placement only on drop.
|
|
|
|
|
if (!dropTarget || dropTarget === dragging) {
|
|
|
|
|
delete presetsList.dataset.dropTargetId;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
presetsList.dataset.dropTargetId = dropTarget.dataset.presetId || '';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
presetsList.addEventListener('drop', async (e) => {
|
|
|
|
|
if (getPresetUiMode() !== 'edit') return;
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const dragging = presetsList.querySelector('.dragging');
|
|
|
|
|
if (!dragging) return;
|
|
|
|
|
const targetId = presetsList.dataset.dropTargetId;
|
|
|
|
|
if (targetId) {
|
|
|
|
|
const dropTarget = presetsList.querySelector(`.draggable-preset[data-preset-id="${targetId}"]:not(.dragging)`);
|
|
|
|
|
if (dropTarget) {
|
|
|
|
|
insertDraggingOntoTarget(presetsList, dragging, dropTarget);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
delete presetsList.dataset.dropTargetId;
|
|
|
|
|
|
|
|
|
|
const saveId = presetsList.dataset.reorderTabId;
|
|
|
|
|
const presetElements = [...presetsList.querySelectorAll('.draggable-preset')];
|
|
|
|
|
const presetIds = presetElements.map((el) => el.dataset.presetId);
|
|
|
|
|
|
|
|
|
|
const newGrid = arrayToGrid(presetIds, 3);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (!saveId) {
|
|
|
|
|
console.warn('No tab id for preset reorder save');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await savePresetGrid(saveId, newGrid);
|
|
|
|
|
await renderTabPresets(saveId);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to save preset grid:', error);
|
|
|
|
|
alert('Failed to save preset order. Please try again.');
|
|
|
|
|
const fallbackId = presetsList.dataset.reorderTabId;
|
|
|
|
|
if (fallbackId) await renderTabPresets(fallbackId);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the currently selected preset for this tab
|
|
|
|
|
const selectedPresetId = selectedPresets[tabId];
|
|
|
|
|
@@ -1543,7 +1686,11 @@ const renderTabPresets = async (tabId) => {
|
|
|
|
|
const preset = allPresets[presetId];
|
|
|
|
|
if (preset) {
|
|
|
|
|
const isSelected = presetId === selectedPresetId;
|
|
|
|
|
const wrapper = createPresetButton(presetId, preset, tabId, isSelected);
|
|
|
|
|
const displayPreset = {
|
|
|
|
|
...preset,
|
|
|
|
|
colors: resolveColorsWithPaletteRefs(preset.colors, preset.palette_refs, paletteColors),
|
|
|
|
|
};
|
|
|
|
|
const wrapper = createPresetButton(presetId, displayPreset, tabId, isSelected);
|
|
|
|
|
presetsList.appendChild(wrapper);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
@@ -1555,15 +1702,22 @@ const renderTabPresets = async (tabId) => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
|
|
|
|
|
const uiMode = getPresetUiMode();
|
|
|
|
|
|
|
|
|
|
const row = document.createElement('div');
|
|
|
|
|
const canDrag = uiMode === 'edit';
|
|
|
|
|
row.className = `preset-tile-row preset-tile-row--${uiMode}${canDrag ? ' draggable-preset' : ''}`;
|
|
|
|
|
row.draggable = canDrag;
|
|
|
|
|
row.dataset.presetId = presetId;
|
|
|
|
|
|
|
|
|
|
const button = document.createElement('button');
|
|
|
|
|
button.className = 'pattern-button draggable-preset';
|
|
|
|
|
button.draggable = true;
|
|
|
|
|
button.dataset.presetId = presetId;
|
|
|
|
|
button.type = 'button';
|
|
|
|
|
button.className = 'pattern-button preset-tile-main';
|
|
|
|
|
if (isSelected) {
|
|
|
|
|
button.classList.add('active');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const colors = Array.isArray(preset.colors) ? preset.colors.filter(c => c) : [];
|
|
|
|
|
const colors = Array.isArray(preset.colors) ? preset.colors.filter((c) => c) : [];
|
|
|
|
|
const isRainbow = (preset.pattern || '').toLowerCase() === 'rainbow';
|
|
|
|
|
const barColors = isRainbow
|
|
|
|
|
? ['#FF0000', '#FF7F00', '#FFFF00', '#00FF00', '#0000FF', '#4B0082', '#8F00FF']
|
|
|
|
|
@@ -1584,38 +1738,76 @@ const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
|
|
|
|
|
presetNameLabel.className = 'pattern-button-label';
|
|
|
|
|
button.appendChild(presetNameLabel);
|
|
|
|
|
|
|
|
|
|
button.addEventListener('click', (e) => {
|
|
|
|
|
button.addEventListener('click', () => {
|
|
|
|
|
if (isDraggingPreset) return;
|
|
|
|
|
const presetsList = document.getElementById('presets-list-tab');
|
|
|
|
|
if (presetsList) {
|
|
|
|
|
presetsList.querySelectorAll('.pattern-button').forEach(btn => btn.classList.remove('active'));
|
|
|
|
|
const presetsListEl = document.getElementById('presets-list-tab');
|
|
|
|
|
if (presetsListEl) {
|
|
|
|
|
presetsListEl.querySelectorAll('.pattern-button').forEach((btn) => btn.classList.remove('active'));
|
|
|
|
|
}
|
|
|
|
|
button.classList.add('active');
|
|
|
|
|
selectedPresets[tabId] = presetId;
|
|
|
|
|
const section = button.closest('.presets-section');
|
|
|
|
|
const section = row.closest('.presets-section');
|
|
|
|
|
sendSelectForCurrentTabDevices(presetId, section);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
button.addEventListener('contextmenu', async (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (isDraggingPreset) return;
|
|
|
|
|
await editPresetFromTab(presetId, tabId, preset);
|
|
|
|
|
});
|
|
|
|
|
if (canDrag) {
|
|
|
|
|
row.addEventListener('dragstart', (e) => {
|
|
|
|
|
isDraggingPreset = true;
|
|
|
|
|
row.classList.add('dragging');
|
|
|
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
|
|
|
e.dataTransfer.setData('text/plain', presetId);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
button.addEventListener('dragstart', (e) => {
|
|
|
|
|
isDraggingPreset = true;
|
|
|
|
|
button.classList.add('dragging');
|
|
|
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
|
|
|
e.dataTransfer.setData('text/plain', presetId);
|
|
|
|
|
});
|
|
|
|
|
row.addEventListener('dragend', () => {
|
|
|
|
|
row.classList.remove('dragging');
|
|
|
|
|
const presetsListEl = document.getElementById('presets-list-tab');
|
|
|
|
|
if (presetsListEl) {
|
|
|
|
|
delete presetsListEl.dataset.dropTargetId;
|
|
|
|
|
}
|
|
|
|
|
document.querySelectorAll('.draggable-preset').forEach((el) => el.classList.remove('drag-over'));
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
isDraggingPreset = false;
|
|
|
|
|
}, 100);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
button.addEventListener('dragend', (e) => {
|
|
|
|
|
button.classList.remove('dragging');
|
|
|
|
|
document.querySelectorAll('.draggable-preset').forEach(el => el.classList.remove('drag-over'));
|
|
|
|
|
setTimeout(() => { isDraggingPreset = false; }, 100);
|
|
|
|
|
});
|
|
|
|
|
row.appendChild(button);
|
|
|
|
|
|
|
|
|
|
return button;
|
|
|
|
|
if (uiMode === 'edit') {
|
|
|
|
|
const actions = document.createElement('div');
|
|
|
|
|
actions.className = 'preset-tile-actions';
|
|
|
|
|
|
|
|
|
|
const editBtn = document.createElement('button');
|
|
|
|
|
editBtn.type = 'button';
|
|
|
|
|
editBtn.className = 'btn btn-secondary btn-small';
|
|
|
|
|
editBtn.textContent = 'Edit';
|
|
|
|
|
editBtn.title = 'Edit preset';
|
|
|
|
|
editBtn.addEventListener('click', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
if (isDraggingPreset) return;
|
|
|
|
|
editPresetFromTab(presetId, tabId, preset);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const removeBtn = document.createElement('button');
|
|
|
|
|
removeBtn.type = 'button';
|
|
|
|
|
removeBtn.className = 'btn btn-danger btn-small';
|
|
|
|
|
removeBtn.textContent = 'Remove';
|
|
|
|
|
removeBtn.title = 'Remove from this tab';
|
|
|
|
|
removeBtn.addEventListener('click', async (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
if (isDraggingPreset) return;
|
|
|
|
|
if (!window.confirm('Remove this preset from this tab?')) return;
|
|
|
|
|
await removePresetFromTab(tabId, presetId);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
actions.appendChild(editBtn);
|
|
|
|
|
actions.appendChild(removeBtn);
|
|
|
|
|
row.appendChild(actions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return row;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const editPresetFromTab = async (presetId, tabId, existingPreset) => {
|
|
|
|
|
@@ -1731,3 +1923,20 @@ document.body.addEventListener('htmx:afterSwap', (event) => {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
updateUiModeToggleButtons();
|
|
|
|
|
document.querySelectorAll('.ui-mode-toggle').forEach((btn) => {
|
|
|
|
|
btn.addEventListener('click', () => {
|
|
|
|
|
const next = getPresetUiMode() === 'edit' ? 'run' : 'edit';
|
|
|
|
|
setPresetUiMode(next);
|
|
|
|
|
updateUiModeToggleButtons();
|
|
|
|
|
const mainMenu = document.getElementById('main-menu-dropdown');
|
|
|
|
|
if (mainMenu) mainMenu.classList.remove('open');
|
|
|
|
|
const leftPanel = document.querySelector('.presets-section[data-tab-id]');
|
|
|
|
|
if (leftPanel) {
|
|
|
|
|
renderTabPresets(leftPanel.dataset.tabId);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|