Add presets system and convert back to Flask
- Convert from Microdot back to Flask - Add presets system with CRUD operations - Store presets in presets.json file - Replace patterns section with presets grid - Add preset editor with full configuration - Add collapse/expand functionality to left panel - Always show on/off presets in presets list - Highlight active preset matching current tab settings - Add 'Create from Current' button in preset editor
This commit is contained in:
610
static/app.js
610
static/app.js
@@ -5,7 +5,8 @@ class LightingController {
|
||||
this.state = {
|
||||
lights: {},
|
||||
patterns: {},
|
||||
tab_order: []
|
||||
tab_order: [],
|
||||
presets: {}
|
||||
};
|
||||
this.selectedColorIndex = 0;
|
||||
this.updateTimeouts = {};
|
||||
@@ -28,6 +29,10 @@ class LightingController {
|
||||
const response = await fetch('/api/state');
|
||||
const data = await response.json();
|
||||
this.state = data;
|
||||
// Ensure presets is always an object
|
||||
if (!this.state.presets) {
|
||||
this.state.presets = {};
|
||||
}
|
||||
// Update current profile display
|
||||
this.updateCurrentProfileDisplay();
|
||||
// Update current profile display if profiles modal is open
|
||||
@@ -54,6 +59,7 @@ class LightingController {
|
||||
document.getElementById('edit-tab-btn').addEventListener('click', () => this.showEditTabModal());
|
||||
document.getElementById('delete-tab-btn').addEventListener('click', () => this.deleteCurrentTab());
|
||||
document.getElementById('color-palette-btn').addEventListener('click', () => this.showColorPalette());
|
||||
document.getElementById('presets-btn').addEventListener('click', () => this.showPresets());
|
||||
document.getElementById('profiles-btn').addEventListener('click', () => this.showProfiles());
|
||||
|
||||
// Modal actions
|
||||
@@ -63,11 +69,22 @@ class LightingController {
|
||||
document.getElementById('edit-tab-cancel').addEventListener('click', () => this.hideModal('edit-tab-modal'));
|
||||
document.getElementById('profiles-close-btn').addEventListener('click', () => this.hideModal('profiles-modal'));
|
||||
document.getElementById('color-palette-close-btn').addEventListener('click', () => this.hideModal('color-palette-modal'));
|
||||
document.getElementById('presets-close-btn').addEventListener('click', () => this.hideModal('presets-modal'));
|
||||
document.getElementById('create-preset-btn').addEventListener('click', () => this.showPresetEditor());
|
||||
document.getElementById('preset-editor-save-btn').addEventListener('click', () => this.savePreset());
|
||||
document.getElementById('preset-editor-cancel-btn').addEventListener('click', () => this.hideModal('preset-editor-modal'));
|
||||
document.getElementById('preset-add-color-btn').addEventListener('click', () => this.addPresetColor());
|
||||
document.getElementById('preset-remove-color-btn').addEventListener('click', () => this.removePresetColor());
|
||||
document.getElementById('preset-editor-from-current-btn').addEventListener('click', () => this.loadCurrentTabToPresetEditor());
|
||||
document.getElementById('preset-brightness-slider').addEventListener('input', (e) => {
|
||||
document.getElementById('preset-brightness-value').textContent = e.target.value;
|
||||
});
|
||||
document.getElementById('quick-palette-close-btn').addEventListener('click', () => this.hideQuickPaletteModal());
|
||||
document.getElementById('quick-palette-use-picker-btn').addEventListener('click', () => this.useColorPickerFromQuickPalette());
|
||||
document.getElementById('create-profile-btn').addEventListener('click', () => this.createProfile());
|
||||
document.getElementById('add-palette-color-btn').addEventListener('click', () => this.addPaletteColor());
|
||||
document.getElementById('palette-add-color-btn').addEventListener('click', () => this.addPaletteColorFromModal());
|
||||
document.getElementById('toggle-left-panel').addEventListener('click', () => this.toggleLeftPanel());
|
||||
|
||||
// Enter key for new profile name
|
||||
document.getElementById('new-profile-name').addEventListener('keypress', (e) => {
|
||||
@@ -107,6 +124,12 @@ class LightingController {
|
||||
document.getElementById('profiles-modal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'profiles-modal') this.hideModal('profiles-modal');
|
||||
});
|
||||
document.getElementById('presets-modal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'presets-modal') this.hideModal('presets-modal');
|
||||
});
|
||||
document.getElementById('preset-editor-modal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'preset-editor-modal') this.hideModal('preset-editor-modal');
|
||||
});
|
||||
}
|
||||
|
||||
renderTabs() {
|
||||
@@ -125,6 +148,12 @@ class LightingController {
|
||||
});
|
||||
}
|
||||
|
||||
toggleLeftPanel() {
|
||||
const leftPanel = document.querySelector('.left-panel');
|
||||
if (!leftPanel) return;
|
||||
leftPanel.classList.toggle('collapsed');
|
||||
}
|
||||
|
||||
async selectTab(tabName) {
|
||||
if (!this.state.lights[tabName]) return;
|
||||
|
||||
@@ -169,25 +198,228 @@ class LightingController {
|
||||
document.getElementById(`n${i}-input`).value = patternSettings[`n${i}`] || 10;
|
||||
}
|
||||
|
||||
// Render patterns
|
||||
this.renderPatterns(tabName, pattern);
|
||||
// Render presets
|
||||
this.renderPresets(tabName);
|
||||
}
|
||||
|
||||
renderPatterns(tabName, activePattern) {
|
||||
const patternsList = document.getElementById('patterns-list');
|
||||
patternsList.innerHTML = '';
|
||||
renderPresets(tabName) {
|
||||
const presetsList = document.getElementById('presets-list-tab');
|
||||
presetsList.innerHTML = '';
|
||||
|
||||
Object.keys(this.state.patterns).forEach(patternName => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'pattern-button';
|
||||
button.textContent = patternName;
|
||||
if (patternName === activePattern) {
|
||||
button.classList.add('active');
|
||||
const presets = this.state.presets || {};
|
||||
const presetNames = Object.keys(presets);
|
||||
|
||||
// Get current tab's settings for comparison
|
||||
const currentSettings = this.getCurrentTabSettings(tabName);
|
||||
|
||||
// Always include "on" and "off" presets
|
||||
const defaultPresets = {
|
||||
'on': {
|
||||
pattern: 'on',
|
||||
colors: ['#FFFFFF'],
|
||||
brightness: 255,
|
||||
delay: 100,
|
||||
n1: 10,
|
||||
n2: 10,
|
||||
n3: 10,
|
||||
n4: 10
|
||||
},
|
||||
'off': {
|
||||
pattern: 'off',
|
||||
colors: ['#000000'],
|
||||
brightness: 0,
|
||||
delay: 100,
|
||||
n1: 10,
|
||||
n2: 10,
|
||||
n3: 10,
|
||||
n4: 10
|
||||
}
|
||||
button.addEventListener('click', () => this.setPattern(tabName, patternName));
|
||||
patternsList.appendChild(button);
|
||||
};
|
||||
|
||||
// Create a combined list with default presets first, then user presets
|
||||
const allPresets = { ...defaultPresets, ...presets };
|
||||
const allPresetNames = ['on', 'off', ...presetNames.filter(name => name !== 'on' && name !== 'off')];
|
||||
|
||||
allPresetNames.forEach(presetName => {
|
||||
const preset = allPresets[presetName];
|
||||
const presetButton = document.createElement('button');
|
||||
presetButton.className = 'pattern-button';
|
||||
|
||||
// Check if this preset matches the current tab's settings
|
||||
const isActive = this.presetMatchesSettings(preset, currentSettings);
|
||||
if (isActive) {
|
||||
presetButton.classList.add('active');
|
||||
}
|
||||
|
||||
// Mark default presets (on/off) with a special indicator
|
||||
const isDefault = presetName === 'on' || presetName === 'off';
|
||||
if (isDefault) {
|
||||
presetButton.classList.add('default-preset');
|
||||
}
|
||||
|
||||
// Create preset info display
|
||||
const presetInfo = document.createElement('div');
|
||||
presetInfo.style.cssText = 'display: flex; flex-direction: column; align-items: flex-start; width: 100%;';
|
||||
|
||||
const presetNameLabel = document.createElement('span');
|
||||
presetNameLabel.textContent = presetName;
|
||||
presetNameLabel.style.fontWeight = 'bold';
|
||||
presetNameLabel.style.marginBottom = '0.25rem';
|
||||
|
||||
const presetDetails = document.createElement('span');
|
||||
presetDetails.style.fontSize = '0.85em';
|
||||
presetDetails.style.color = '#aaa';
|
||||
presetDetails.textContent = `${preset.pattern} • ${preset.colors.length} color${preset.colors.length !== 1 ? 's' : ''}`;
|
||||
|
||||
presetInfo.appendChild(presetNameLabel);
|
||||
presetInfo.appendChild(presetDetails);
|
||||
presetButton.appendChild(presetInfo);
|
||||
|
||||
presetButton.addEventListener('click', () => {
|
||||
if (isDefault && !presets[presetName]) {
|
||||
// Apply default preset directly
|
||||
this.applyDefaultPreset(tabName, presetName, preset);
|
||||
} else {
|
||||
// Apply regular preset
|
||||
this.applyPresetToTab(tabName, presetName);
|
||||
}
|
||||
});
|
||||
presetsList.appendChild(presetButton);
|
||||
});
|
||||
}
|
||||
|
||||
async applyDefaultPreset(tabName, presetName, preset) {
|
||||
// Apply default preset by setting pattern and parameters directly
|
||||
try {
|
||||
// First set the pattern
|
||||
const patternResponse = await fetch('/api/pattern', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tab_name: tabName,
|
||||
pattern: preset.pattern,
|
||||
delay: preset.delay,
|
||||
colors: preset.colors,
|
||||
n1: preset.n1,
|
||||
n2: preset.n2,
|
||||
n3: preset.n3,
|
||||
n4: preset.n4
|
||||
})
|
||||
});
|
||||
|
||||
if (patternResponse.ok) {
|
||||
// Then update brightness
|
||||
await fetch('/api/parameters', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tab_name: tabName,
|
||||
brightness: preset.brightness
|
||||
})
|
||||
});
|
||||
|
||||
// Reload state and tab content
|
||||
await this.loadState();
|
||||
await this.loadTabContent(tabName);
|
||||
} else {
|
||||
const error = await patternResponse.json();
|
||||
alert(`Failed to apply preset: ${error.error || 'Unknown error'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to apply default preset:', error);
|
||||
alert('Failed to apply preset');
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentTabSettings(tabName) {
|
||||
if (!this.state.lights[tabName]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const light = this.state.lights[tabName];
|
||||
const settings = light.settings;
|
||||
const pattern = settings.pattern || 'on';
|
||||
const patternSettings = this.getPatternSettings(tabName, pattern);
|
||||
|
||||
return {
|
||||
pattern: pattern,
|
||||
brightness: settings.brightness || 127,
|
||||
delay: patternSettings.delay || 100,
|
||||
colors: patternSettings.colors || ['#000000'],
|
||||
n1: patternSettings.n1 || 10,
|
||||
n2: patternSettings.n2 || 10,
|
||||
n3: patternSettings.n3 || 10,
|
||||
n4: patternSettings.n4 || 10
|
||||
};
|
||||
}
|
||||
|
||||
presetMatchesSettings(preset, currentSettings) {
|
||||
if (!currentSettings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare all settings
|
||||
if (preset.pattern !== currentSettings.pattern) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preset.brightness !== currentSettings.brightness) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preset.delay !== currentSettings.delay) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare n values
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
if (preset[`n${i}`] !== currentSettings[`n${i}`]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Compare colors (order matters for presets)
|
||||
if (preset.colors.length !== currentSettings.colors.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < preset.colors.length; i++) {
|
||||
// Normalize color format (uppercase, no spaces)
|
||||
const presetColor = preset.colors[i].toUpperCase().trim();
|
||||
const currentColor = currentSettings.colors[i].toUpperCase().trim();
|
||||
if (presetColor !== currentColor) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async applyPresetToTab(tabName, presetName) {
|
||||
try {
|
||||
const response = await fetch(`/api/presets/${presetName}/apply`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tab_name: tabName
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Reload state and tab content to reflect changes
|
||||
await this.loadState();
|
||||
await this.loadTabContent(tabName);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`Failed to apply preset: ${error.error || 'Unknown error'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to apply preset:', error);
|
||||
alert('Failed to apply preset');
|
||||
}
|
||||
}
|
||||
|
||||
renderColorPalette(tabName, colors) {
|
||||
const palette = document.getElementById('color-palette');
|
||||
@@ -1056,6 +1288,356 @@ class LightingController {
|
||||
}
|
||||
}
|
||||
|
||||
async showPresets() {
|
||||
const modal = document.getElementById('presets-modal');
|
||||
modal.classList.add('active');
|
||||
await this.loadPresets();
|
||||
}
|
||||
|
||||
async loadPresets() {
|
||||
try {
|
||||
const response = await fetch('/api/presets');
|
||||
const data = await response.json();
|
||||
|
||||
this.state.presets = data.presets || {};
|
||||
|
||||
const presetsList = document.getElementById('presets-list');
|
||||
presetsList.innerHTML = '';
|
||||
|
||||
const presetNames = Object.keys(this.state.presets);
|
||||
if (presetNames.length === 0) {
|
||||
presetsList.innerHTML = '<p style="text-align: center; color: #888;">No presets found. Create one to get started.</p>';
|
||||
} else {
|
||||
presetNames.forEach(presetName => {
|
||||
const preset = this.state.presets[presetName];
|
||||
const presetItem = document.createElement('div');
|
||||
presetItem.style.cssText = 'display: flex; align-items: center; justify-content: space-between; padding: 0.75rem; background-color: #3a3a3a; border-radius: 4px; margin-bottom: 0.5rem;';
|
||||
|
||||
const presetInfo = document.createElement('div');
|
||||
presetInfo.style.cssText = 'display: flex; flex-direction: column; gap: 0.25rem; flex: 1;';
|
||||
|
||||
const presetNameLabel = document.createElement('span');
|
||||
presetNameLabel.textContent = presetName;
|
||||
presetNameLabel.style.fontWeight = 'bold';
|
||||
|
||||
const presetDetails = document.createElement('span');
|
||||
presetDetails.style.fontSize = '0.9em';
|
||||
presetDetails.style.color = '#aaa';
|
||||
presetDetails.textContent = `Pattern: ${preset.pattern} | Brightness: ${preset.brightness} | Delay: ${preset.delay}ms | Colors: ${preset.colors.length}`;
|
||||
|
||||
presetInfo.appendChild(presetNameLabel);
|
||||
presetInfo.appendChild(presetDetails);
|
||||
|
||||
const actionsContainer = document.createElement('div');
|
||||
actionsContainer.style.cssText = 'display: flex; gap: 0.5rem;';
|
||||
|
||||
const applyButton = document.createElement('button');
|
||||
applyButton.className = 'btn btn-small btn-primary';
|
||||
applyButton.textContent = 'Apply';
|
||||
applyButton.addEventListener('click', () => this.applyPreset(presetName));
|
||||
|
||||
const editButton = document.createElement('button');
|
||||
editButton.className = 'btn btn-small';
|
||||
editButton.textContent = 'Edit';
|
||||
editButton.addEventListener('click', () => this.showPresetEditor(presetName));
|
||||
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.className = 'btn btn-small btn-danger';
|
||||
deleteButton.textContent = 'Delete';
|
||||
deleteButton.addEventListener('click', () => this.deletePreset(presetName));
|
||||
|
||||
actionsContainer.appendChild(applyButton);
|
||||
actionsContainer.appendChild(editButton);
|
||||
actionsContainer.appendChild(deleteButton);
|
||||
|
||||
presetItem.appendChild(presetInfo);
|
||||
presetItem.appendChild(actionsContainer);
|
||||
presetsList.appendChild(presetItem);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load presets:', error);
|
||||
alert('Failed to load presets');
|
||||
}
|
||||
}
|
||||
|
||||
async applyPreset(presetName) {
|
||||
if (!this.currentTab) {
|
||||
alert('Please select a tab first');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/presets/${presetName}/apply`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tab_name: this.currentTab
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await this.loadState();
|
||||
await this.loadTabContent(this.currentTab);
|
||||
this.hideModal('presets-modal');
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`Failed to apply preset: ${error.error || 'Unknown error'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to apply preset:', error);
|
||||
alert('Failed to apply preset');
|
||||
}
|
||||
}
|
||||
|
||||
async deletePreset(presetName) {
|
||||
if (!confirm(`Delete preset '${presetName}'? This cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/presets/${presetName}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await this.loadPresets();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`Failed to delete preset: ${error.error || 'Unknown error'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete preset:', error);
|
||||
alert('Failed to delete preset');
|
||||
}
|
||||
}
|
||||
|
||||
showPresetEditor(presetName = null) {
|
||||
const modal = document.getElementById('preset-editor-modal');
|
||||
const title = document.getElementById('preset-editor-title');
|
||||
const nameInput = document.getElementById('preset-name-input');
|
||||
const patternSelect = document.getElementById('preset-pattern-select');
|
||||
const brightnessSlider = document.getElementById('preset-brightness-slider');
|
||||
const brightnessValue = document.getElementById('preset-brightness-value');
|
||||
const delayInput = document.getElementById('preset-delay-input');
|
||||
const n1Input = document.getElementById('preset-n1-input');
|
||||
const n2Input = document.getElementById('preset-n2-input');
|
||||
const n3Input = document.getElementById('preset-n3-input');
|
||||
const n4Input = document.getElementById('preset-n4-input');
|
||||
|
||||
// Store editing preset name
|
||||
this.editingPresetName = presetName;
|
||||
|
||||
// Populate pattern select
|
||||
patternSelect.innerHTML = '';
|
||||
const patterns = Object.keys(this.state.patterns || {});
|
||||
patterns.forEach(pattern => {
|
||||
const option = document.createElement('option');
|
||||
option.value = pattern;
|
||||
option.textContent = pattern;
|
||||
patternSelect.appendChild(option);
|
||||
});
|
||||
|
||||
if (presetName && this.state.presets[presetName]) {
|
||||
// Editing existing preset
|
||||
title.textContent = 'Edit Preset';
|
||||
const preset = this.state.presets[presetName];
|
||||
nameInput.value = presetName;
|
||||
nameInput.disabled = false;
|
||||
patternSelect.value = preset.pattern;
|
||||
brightnessSlider.value = preset.brightness;
|
||||
brightnessValue.textContent = preset.brightness;
|
||||
delayInput.value = preset.delay;
|
||||
n1Input.value = preset.n1;
|
||||
n2Input.value = preset.n2;
|
||||
n3Input.value = preset.n3;
|
||||
n4Input.value = preset.n4;
|
||||
this.renderPresetColors(preset.colors);
|
||||
} else {
|
||||
// Creating new preset
|
||||
title.textContent = 'Create Preset';
|
||||
nameInput.value = '';
|
||||
nameInput.disabled = false;
|
||||
patternSelect.value = patterns[0] || 'on';
|
||||
brightnessSlider.value = 127;
|
||||
brightnessValue.textContent = 127;
|
||||
delayInput.value = 100;
|
||||
n1Input.value = 10;
|
||||
n2Input.value = 10;
|
||||
n3Input.value = 10;
|
||||
n4Input.value = 10;
|
||||
this.renderPresetColors(['#000000']);
|
||||
}
|
||||
|
||||
this.selectedPresetColorIndex = 0;
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
renderPresetColors(colors) {
|
||||
const container = document.getElementById('preset-colors-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
colors.forEach((color, index) => {
|
||||
const colorWrapper = document.createElement('div');
|
||||
colorWrapper.style.cssText = 'position: relative;';
|
||||
|
||||
const colorSwatch = document.createElement('div');
|
||||
colorSwatch.style.cssText = `width: 50px; height: 50px; background-color: ${color}; border: 2px solid ${index === this.selectedPresetColorIndex ? '#FFD700' : '#4a4a4a'}; border-radius: 4px; cursor: pointer; position: relative;`;
|
||||
colorSwatch.dataset.index = index;
|
||||
colorSwatch.addEventListener('click', () => {
|
||||
this.selectedPresetColorIndex = index;
|
||||
this.renderPresetColors(colors);
|
||||
});
|
||||
|
||||
const colorPicker = document.createElement('input');
|
||||
colorPicker.type = 'color';
|
||||
colorPicker.value = color;
|
||||
colorPicker.style.cssText = 'position: absolute; top: 0; left: 0; width: 50px; height: 50px; opacity: 0; cursor: pointer;';
|
||||
colorPicker.addEventListener('change', (e) => {
|
||||
colors[index] = e.target.value;
|
||||
this.renderPresetColors(colors);
|
||||
});
|
||||
|
||||
colorWrapper.appendChild(colorSwatch);
|
||||
colorWrapper.appendChild(colorPicker);
|
||||
container.appendChild(colorWrapper);
|
||||
});
|
||||
|
||||
this.presetColors = colors;
|
||||
}
|
||||
|
||||
addPresetColor() {
|
||||
const colorInput = document.getElementById('preset-new-color');
|
||||
const color = colorInput.value;
|
||||
|
||||
if (!this.presetColors) {
|
||||
this.presetColors = [];
|
||||
}
|
||||
|
||||
this.presetColors.push(color);
|
||||
this.selectedPresetColorIndex = this.presetColors.length - 1;
|
||||
this.renderPresetColors(this.presetColors);
|
||||
}
|
||||
|
||||
removePresetColor() {
|
||||
if (!this.presetColors || this.presetColors.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedPresetColorIndex >= 0 && this.selectedPresetColorIndex < this.presetColors.length) {
|
||||
this.presetColors.splice(this.selectedPresetColorIndex, 1);
|
||||
if (this.selectedPresetColorIndex >= this.presetColors.length) {
|
||||
this.selectedPresetColorIndex = Math.max(0, this.presetColors.length - 1);
|
||||
}
|
||||
this.renderPresetColors(this.presetColors);
|
||||
}
|
||||
}
|
||||
|
||||
loadCurrentTabToPresetEditor() {
|
||||
if (!this.currentTab || !this.state.lights[this.currentTab]) {
|
||||
alert('Please select a tab first');
|
||||
return;
|
||||
}
|
||||
|
||||
const light = this.state.lights[this.currentTab];
|
||||
const settings = light.settings;
|
||||
const pattern = settings.pattern || 'on';
|
||||
|
||||
// Get pattern-specific settings
|
||||
const patternSettings = this.getPatternSettings(this.currentTab, pattern);
|
||||
|
||||
const patternSelect = document.getElementById('preset-pattern-select');
|
||||
const brightnessSlider = document.getElementById('preset-brightness-slider');
|
||||
const brightnessValue = document.getElementById('preset-brightness-value');
|
||||
const delayInput = document.getElementById('preset-delay-input');
|
||||
const n1Input = document.getElementById('preset-n1-input');
|
||||
const n2Input = document.getElementById('preset-n2-input');
|
||||
const n3Input = document.getElementById('preset-n3-input');
|
||||
const n4Input = document.getElementById('preset-n4-input');
|
||||
|
||||
patternSelect.value = pattern;
|
||||
brightnessSlider.value = settings.brightness || 127;
|
||||
brightnessValue.textContent = settings.brightness || 127;
|
||||
delayInput.value = patternSettings.delay || 100;
|
||||
n1Input.value = patternSettings.n1 || 10;
|
||||
n2Input.value = patternSettings.n2 || 10;
|
||||
n3Input.value = patternSettings.n3 || 10;
|
||||
n4Input.value = patternSettings.n4 || 10;
|
||||
|
||||
this.renderPresetColors(patternSettings.colors || ['#000000']);
|
||||
}
|
||||
|
||||
async savePreset() {
|
||||
const nameInput = document.getElementById('preset-name-input');
|
||||
const patternSelect = document.getElementById('preset-pattern-select');
|
||||
const brightnessSlider = document.getElementById('preset-brightness-slider');
|
||||
const delayInput = document.getElementById('preset-delay-input');
|
||||
const n1Input = document.getElementById('preset-n1-input');
|
||||
const n2Input = document.getElementById('preset-n2-input');
|
||||
const n3Input = document.getElementById('preset-n3-input');
|
||||
const n4Input = document.getElementById('preset-n4-input');
|
||||
|
||||
const presetName = nameInput.value.trim();
|
||||
if (!presetName) {
|
||||
alert('Please enter a preset name');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.presetColors || this.presetColors.length === 0) {
|
||||
alert('Please add at least one color');
|
||||
return;
|
||||
}
|
||||
|
||||
const presetData = {
|
||||
name: presetName,
|
||||
pattern: patternSelect.value,
|
||||
brightness: parseInt(brightnessSlider.value),
|
||||
delay: parseInt(delayInput.value),
|
||||
n1: parseInt(n1Input.value),
|
||||
n2: parseInt(n2Input.value),
|
||||
n3: parseInt(n3Input.value),
|
||||
n4: parseInt(n4Input.value),
|
||||
colors: this.presetColors
|
||||
};
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (this.editingPresetName) {
|
||||
// Update existing preset
|
||||
response = await fetch(`/api/presets/${this.editingPresetName}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(presetData)
|
||||
});
|
||||
} else {
|
||||
// Create new preset
|
||||
response = await fetch('/api/presets', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(presetData)
|
||||
});
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
await this.loadPresets();
|
||||
this.hideModal('preset-editor-modal');
|
||||
this.editingPresetName = null;
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`Failed to save preset: ${error.error || 'Unknown error'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save preset:', error);
|
||||
alert('Failed to save preset');
|
||||
}
|
||||
}
|
||||
|
||||
hideModal(modalId) {
|
||||
document.getElementById(modalId).classList.remove('active');
|
||||
}
|
||||
|
||||
@@ -153,6 +153,37 @@ header h1 {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.left-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.left-panel-toggle {
|
||||
padding: 0.25rem 0.5rem;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.left-panel-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.left-panel.collapsed {
|
||||
flex: 0 0 48px;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.left-panel.collapsed .left-panel-body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.left-panel.collapsed .left-panel-toggle {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.controls-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -310,6 +341,7 @@ header h1 {
|
||||
}
|
||||
|
||||
.patterns-section,
|
||||
.presets-section,
|
||||
.color-palette-section {
|
||||
background-color: #1a1a1a;
|
||||
border: 2px solid #4a4a4a;
|
||||
@@ -318,6 +350,7 @@ header h1 {
|
||||
}
|
||||
|
||||
.patterns-section h3,
|
||||
.presets-section h3,
|
||||
.color-palette-section h3 {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
@@ -329,6 +362,12 @@ header h1 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.presets-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.pattern-button {
|
||||
padding: 0.75rem;
|
||||
background-color: #3a3a3a;
|
||||
@@ -350,6 +389,10 @@ header h1 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pattern-button.default-preset {
|
||||
border: 2px solid #6a5acd;
|
||||
}
|
||||
|
||||
.color-palette {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user