Update UI for palettes, presets, and patterns

This commit is contained in:
2026-01-16 22:31:36 +13:00
parent 9c43a0a22b
commit df37f15f73
8 changed files with 3649 additions and 11 deletions

381
src/static/presets.js Normal file
View File

@@ -0,0 +1,381 @@
document.addEventListener('DOMContentLoaded', () => {
const presetsButton = document.getElementById('presets-btn');
const presetsModal = document.getElementById('presets-modal');
const presetsCloseButton = document.getElementById('presets-close-btn');
const presetsList = document.getElementById('presets-list');
const presetsAddButton = document.getElementById('preset-add-btn');
const presetEditorModal = document.getElementById('preset-editor-modal');
const presetEditorCloseButton = document.getElementById('preset-editor-close-btn');
const presetNameInput = document.getElementById('preset-name-input');
const presetPatternInput = document.getElementById('preset-pattern-input');
const presetColorsInput = document.getElementById('preset-colors-input');
const presetBrightnessInput = document.getElementById('preset-brightness-input');
const presetDelayInput = document.getElementById('preset-delay-input');
const presetSaveButton = document.getElementById('preset-save-btn');
const presetClearButton = document.getElementById('preset-clear-btn');
const presetAddFromPaletteButton = document.getElementById('preset-add-from-palette-btn');
if (!presetsButton || !presetsModal || !presetsList || !presetSaveButton || !presetClearButton) {
return;
}
let currentEditId = null;
let cachedPresets = {};
let cachedPatterns = {};
const getNumberInput = (id) => {
const input = document.getElementById(id);
if (!input) {
return 0;
}
return parseInt(input.value, 10) || 0;
};
const parseColors = (value) => {
if (!value) {
return [];
}
return value
.split(',')
.map((color) => color.trim())
.filter((color) => color.length > 0)
.map((color) => (color.startsWith('#') ? color : `#${color}`));
};
const setFormValues = (preset) => {
if (!presetNameInput || !presetPatternInput || !presetColorsInput || !presetBrightnessInput || !presetDelayInput) {
return;
}
presetNameInput.value = preset.name || '';
presetPatternInput.value = preset.pattern || '';
presetColorsInput.value = Array.isArray(preset.colors) ? preset.colors.join(',') : '';
presetBrightnessInput.value = preset.brightness || 0;
presetDelayInput.value = preset.delay || 0;
document.getElementById('preset-n1-input').value = preset.n1 || 0;
document.getElementById('preset-n2-input').value = preset.n2 || 0;
document.getElementById('preset-n3-input').value = preset.n3 || 0;
document.getElementById('preset-n4-input').value = preset.n4 || 0;
document.getElementById('preset-n5-input').value = preset.n5 || 0;
document.getElementById('preset-n6-input').value = preset.n6 || 0;
document.getElementById('preset-n7-input').value = preset.n7 || 0;
document.getElementById('preset-n8-input').value = preset.n8 || 0;
};
const clearForm = () => {
currentEditId = null;
setFormValues({
name: '',
pattern: '',
colors: [],
brightness: 0,
delay: 0,
n1: 0,
n2: 0,
n3: 0,
n4: 0,
n5: 0,
n6: 0,
n7: 0,
n8: 0,
});
};
const openEditor = () => {
if (presetEditorModal) {
presetEditorModal.classList.add('active');
}
loadPatterns().then(() => {
updatePresetNLabels(presetPatternInput ? presetPatternInput.value : '');
});
};
const closeEditor = () => {
if (presetEditorModal) {
presetEditorModal.classList.remove('active');
}
};
const buildPresetPayload = () => {
return {
name: presetNameInput ? presetNameInput.value.trim() : '',
pattern: presetPatternInput ? presetPatternInput.value.trim() : '',
colors: parseColors(presetColorsInput ? presetColorsInput.value : ''),
brightness: presetBrightnessInput ? parseInt(presetBrightnessInput.value, 10) || 0 : 0,
delay: presetDelayInput ? parseInt(presetDelayInput.value, 10) || 0 : 0,
n1: getNumberInput('preset-n1-input'),
n2: getNumberInput('preset-n2-input'),
n3: getNumberInput('preset-n3-input'),
n4: getNumberInput('preset-n4-input'),
n5: getNumberInput('preset-n5-input'),
n6: getNumberInput('preset-n6-input'),
n7: getNumberInput('preset-n7-input'),
n8: getNumberInput('preset-n8-input'),
};
};
const loadPatterns = async () => {
if (!presetPatternInput) {
return;
}
try {
const response = await fetch('/patterns', {
headers: { Accept: 'application/json' },
});
if (!response.ok) {
return;
}
const patterns = await response.json();
cachedPatterns = patterns || {};
const entries = Object.keys(cachedPatterns);
const desiredPattern = presetPatternInput.value;
presetPatternInput.innerHTML = '<option value="">Pattern</option>';
entries.forEach((patternName) => {
const option = document.createElement('option');
option.value = patternName;
option.textContent = patternName;
presetPatternInput.appendChild(option);
});
if (desiredPattern && cachedPatterns[desiredPattern]) {
presetPatternInput.value = desiredPattern;
} else if (entries.length > 0) {
let defaultPattern = entries[0];
for (const patternName of entries) {
const config = cachedPatterns[patternName];
const hasMapping = config && Object.values(config).some((value) => {
return typeof value === 'string' && value.startsWith('n');
});
if (hasMapping) {
defaultPattern = patternName;
break;
}
}
presetPatternInput.value = defaultPattern;
}
updatePresetNLabels(presetPatternInput.value);
} catch (error) {
console.warn('Failed to load patterns:', error);
}
};
const updatePresetNLabels = (patternName) => {
const labels = {};
for (let i = 1; i <= 8; i++) {
labels[`n${i}`] = `n${i}:`;
}
const patternConfig = cachedPatterns && cachedPatterns[patternName];
if (patternConfig && typeof patternConfig === 'object') {
Object.entries(patternConfig).forEach(([label, key]) => {
if (typeof key === 'string' && key.startsWith('n')) {
labels[key] = `${label}:`;
}
});
}
for (let i = 1; i <= 8; i++) {
const labelEl = document.getElementById(`preset-n${i}-label`);
if (labelEl) {
labelEl.textContent = labels[`n${i}`];
}
}
};
const renderPresets = (presets) => {
presetsList.innerHTML = '';
cachedPresets = presets || {};
const entries = Object.entries(cachedPresets);
if (!entries.length) {
const empty = document.createElement('p');
empty.className = 'muted-text';
empty.textContent = 'No presets found.';
presetsList.appendChild(empty);
return;
}
entries.forEach(([presetId, preset]) => {
const row = document.createElement('div');
row.className = 'profiles-row';
const label = document.createElement('span');
label.textContent = (preset && preset.name) || presetId;
const details = document.createElement('span');
const pattern = preset && preset.pattern ? preset.pattern : '-';
details.textContent = pattern;
details.style.color = '#aaa';
details.style.fontSize = '0.85em';
const editButton = document.createElement('button');
editButton.className = 'btn btn-secondary btn-small';
editButton.textContent = 'Edit';
editButton.addEventListener('click', () => {
currentEditId = presetId;
setFormValues(preset || {});
openEditor();
});
const deleteButton = document.createElement('button');
deleteButton.className = 'btn btn-danger btn-small';
deleteButton.textContent = 'Delete';
deleteButton.addEventListener('click', async () => {
const confirmed = confirm(`Delete preset "${label.textContent}"?`);
if (!confirmed) {
return;
}
try {
const response = await fetch(`/presets/${presetId}`, {
method: 'DELETE',
headers: { Accept: 'application/json' },
});
if (!response.ok) {
throw new Error('Failed to delete preset');
}
await loadPresets();
if (currentEditId === presetId) {
clearForm();
}
} catch (error) {
console.error('Delete preset failed:', error);
alert('Failed to delete preset.');
}
});
row.appendChild(label);
row.appendChild(details);
row.appendChild(editButton);
row.appendChild(deleteButton);
presetsList.appendChild(row);
});
};
const loadPresets = async () => {
presetsList.innerHTML = '';
const loading = document.createElement('p');
loading.className = 'muted-text';
loading.textContent = 'Loading presets...';
presetsList.appendChild(loading);
try {
const response = await fetch('/presets', {
headers: { Accept: 'application/json' },
});
if (!response.ok) {
throw new Error('Failed to load presets');
}
const presets = await response.json();
renderPresets(presets);
} catch (error) {
console.error('Load presets failed:', error);
presetsList.innerHTML = '';
const errorMessage = document.createElement('p');
errorMessage.className = 'muted-text';
errorMessage.textContent = 'Failed to load presets.';
presetsList.appendChild(errorMessage);
}
};
const openModal = () => {
presetsModal.classList.add('active');
loadPresets();
};
const closeModal = () => {
presetsModal.classList.remove('active');
};
presetsButton.addEventListener('click', openModal);
if (presetsCloseButton) {
presetsCloseButton.addEventListener('click', closeModal);
}
if (presetsAddButton) {
presetsAddButton.addEventListener('click', () => {
clearForm();
openEditor();
});
}
if (presetEditorCloseButton) {
presetEditorCloseButton.addEventListener('click', closeEditor);
}
presetClearButton.addEventListener('click', clearForm);
if (presetPatternInput) {
presetPatternInput.addEventListener('change', () => {
updatePresetNLabels(presetPatternInput.value);
});
}
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 || !presetColorsInput) {
return;
}
const handlePick = (event) => {
const row = event.target.closest('[data-color]');
if (!row) {
return;
}
const picked = row.dataset.color;
if (!picked) {
return;
}
const currentColors = parseColors(presetColorsInput.value);
if (!currentColors.includes(picked)) {
currentColors.push(picked);
presetColorsInput.value = currentColors.join(',');
}
if (modal) {
modal.classList.remove('active');
}
modalList.removeEventListener('click', handlePick);
};
modalList.addEventListener('click', handlePick);
});
}
presetSaveButton.addEventListener('click', async () => {
const payload = buildPresetPayload();
if (!payload.name) {
alert('Preset name is required.');
return;
}
try {
const url = currentEditId ? `/presets/${currentEditId}` : '/presets';
const method = currentEditId ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error('Failed to save preset');
}
await loadPresets();
clearForm();
closeEditor();
} catch (error) {
console.error('Save preset failed:', error);
alert('Failed to save preset.');
}
});
presetsModal.addEventListener('click', (event) => {
if (event.target === presetsModal) {
closeModal();
}
});
if (presetEditorModal) {
presetEditorModal.addEventListener('click', (event) => {
if (event.target === presetEditorModal) {
closeEditor();
}
});
}
clearForm();
});