Files
lighting-controller/static/app.js
Jimmy ce3b9f4ea5 Add profile deletion feature
- Added DELETE endpoint /api/profiles/<profile_name> to delete profiles
- Prevent deletion of the only remaining profile
- Clear current profile state if the active profile is deleted
- Added Delete button next to each profile in the Profiles modal
- Added confirmation dialog before deleting profiles
- Automatically refresh profile list after deletion
2026-01-05 23:09:10 +13:00

1069 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Lighting Controller Web App
class LightingController {
constructor() {
this.currentTab = null;
this.state = {
lights: {},
patterns: {},
tab_order: []
};
this.selectedColorIndex = 0;
this.updateTimeouts = {};
this.quickPaletteContext = null;
this.init();
}
async init() {
await this.loadState();
this.setupEventListeners();
this.renderTabs();
if (this.state.tab_order.length > 0) {
this.selectTab(this.state.tab_order[0]);
}
}
async loadState() {
try {
const response = await fetch('/api/state');
const data = await response.json();
this.state = data;
// Update current profile display
this.updateCurrentProfileDisplay();
// Update current profile display if profiles modal is open
if (document.getElementById('profiles-modal').classList.contains('active')) {
await this.loadProfiles();
await this.loadProfilePalette();
}
} catch (error) {
console.error('Failed to load state:', error);
}
}
updateCurrentProfileDisplay() {
const currentProfileName = document.getElementById('current-profile-name');
if (currentProfileName) {
const profile = this.state.current_profile || 'None';
currentProfileName.textContent = profile;
}
}
setupEventListeners() {
// Tab management
document.getElementById('add-tab-btn').addEventListener('click', () => this.showAddTabModal());
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('profiles-btn').addEventListener('click', () => this.showProfiles());
// Modal actions
document.getElementById('add-tab-confirm').addEventListener('click', () => this.createTab());
document.getElementById('add-tab-cancel').addEventListener('click', () => this.hideModal('add-tab-modal'));
document.getElementById('edit-tab-confirm').addEventListener('click', () => this.updateTab());
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('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());
// Enter key for new profile name
document.getElementById('new-profile-name').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.createProfile();
}
});
// Brightness and delay sliders
document.getElementById('brightness-slider').addEventListener('input', (e) => {
document.getElementById('brightness-value').textContent = e.target.value;
this.debounceUpdate('brightness', () => this.updateBrightness());
});
document.getElementById('delay-slider').addEventListener('input', (e) => {
this.updateDelayValue(e.target.value);
this.debounceUpdate('delay', () => this.updateDelay());
});
// N parameters
for (let i = 1; i <= 4; i++) {
document.getElementById(`n${i}-input`).addEventListener('input', (e) => {
this.debounceUpdate('nparams', () => this.updateNParams());
});
}
// Color palette
document.getElementById('add-color-btn').addEventListener('click', () => this.addColorToPalette());
document.getElementById('remove-color-btn').addEventListener('click', () => this.removeSelectedColor());
// Close modals on outside click
document.getElementById('add-tab-modal').addEventListener('click', (e) => {
if (e.target.id === 'add-tab-modal') this.hideModal('add-tab-modal');
});
document.getElementById('edit-tab-modal').addEventListener('click', (e) => {
if (e.target.id === 'edit-tab-modal') this.hideModal('edit-tab-modal');
});
document.getElementById('profiles-modal').addEventListener('click', (e) => {
if (e.target.id === 'profiles-modal') this.hideModal('profiles-modal');
});
}
renderTabs() {
const tabsList = document.getElementById('tabs-list');
tabsList.innerHTML = '';
this.state.tab_order.forEach(tabName => {
const tabButton = document.createElement('button');
tabButton.className = 'tab-button';
tabButton.textContent = tabName;
tabButton.addEventListener('click', () => this.selectTab(tabName));
if (tabName === this.currentTab) {
tabButton.classList.add('active');
}
tabsList.appendChild(tabButton);
});
}
async selectTab(tabName) {
if (!this.state.lights[tabName]) return;
this.currentTab = tabName;
this.renderTabs();
await this.loadTabContent(tabName);
}
async loadTabContent(tabName) {
const light = this.state.lights[tabName];
if (!light) {
return;
}
const settings = light.settings;
const pattern = settings.pattern || 'on';
// Get pattern-specific settings
const patternSettings = this.getPatternSettings(tabName, pattern);
// Update IDs display
document.getElementById('current-ids').textContent = light.names.join(', ');
// Load and render colors in the palette
const colors = patternSettings.colors || ['#000000'];
this.renderColorPalette(tabName, colors);
// Update brightness slider
const brightness = settings.brightness || 127;
document.getElementById('brightness-slider').value = brightness;
document.getElementById('brightness-value').textContent = brightness;
// Update delay slider
const patternConfig = this.state.patterns[pattern] || {};
const minDelay = patternConfig.min_delay || 10;
const maxDelay = patternConfig.max_delay || 10000;
const delaySliderPos = this.delayToSlider(patternSettings.delay, minDelay, maxDelay);
document.getElementById('delay-slider').value = delaySliderPos;
this.updateDelayValue(delaySliderPos, minDelay, maxDelay);
// Update n parameters
for (let i = 1; i <= 4; i++) {
document.getElementById(`n${i}-input`).value = patternSettings[`n${i}`] || 10;
}
// Render patterns
this.renderPatterns(tabName, pattern);
}
renderPatterns(tabName, activePattern) {
const patternsList = document.getElementById('patterns-list');
patternsList.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');
}
button.addEventListener('click', () => this.setPattern(tabName, patternName));
patternsList.appendChild(button);
});
}
renderColorPalette(tabName, colors) {
const palette = document.getElementById('color-palette');
if (!palette) {
return;
}
palette.innerHTML = '';
colors.forEach((hexColor, index) => {
const swatch = document.createElement('div');
swatch.className = 'color-swatch';
if (index === this.selectedColorIndex) {
swatch.classList.add('selected');
}
const preview = document.createElement('div');
preview.className = 'color-swatch-preview';
preview.style.backgroundColor = hexColor;
const label = document.createElement('span');
label.className = 'color-swatch-label';
label.textContent = `Color ${index + 1}`;
// Color picker input with palette quick-select
const colorPickerWrapper = document.createElement('div');
colorPickerWrapper.style.cssText = 'position: relative; display: inline-block;';
const colorPicker = document.createElement('input');
colorPicker.type = 'color';
colorPicker.value = hexColor;
colorPicker.className = 'color-picker-input';
colorPicker.dataset.tabName = tabName;
colorPicker.dataset.colorIndex = index;
colorPicker.addEventListener('change', (e) => {
const newColor = e.target.value;
this.updateColorInPalette(tabName, index, newColor);
});
// Show quick palette on click, prevent native picker if palette has colors
const clickHandler = (e) => {
e.stopPropagation();
const palette = this.state.color_palette || [];
// Check if we're allowing native picker (set by "Use Color Picker" button)
if (palette.length > 0 && !colorPicker.dataset.allowNative) {
e.preventDefault();
this.showPaletteQuickSelect(colorPickerWrapper, tabName, index, colorPicker);
}
// If no palette colors or allowNative is set, let native picker open
};
colorPicker.addEventListener('click', clickHandler);
// Store handler reference for later removal if needed
colorPicker._clickHandler = clickHandler;
colorPickerWrapper.appendChild(colorPicker);
swatch.appendChild(preview);
swatch.appendChild(label);
swatch.appendChild(colorPickerWrapper);
swatch.addEventListener('click', (e) => {
// Don't trigger selection if clicking on the color picker
if (e.target !== colorPicker && !colorPicker.contains(e.target)) {
this.selectColor(tabName, index, hexColor);
}
});
palette.appendChild(swatch);
});
}
getPatternSettings(tabName, patternName) {
const light = this.state.lights[tabName];
if (!light) return { colors: ['#000000'], delay: 100, n1: 10, n2: 10, n3: 10, n4: 10 };
const lightSettings = light.settings;
if (!lightSettings.patterns) lightSettings.patterns = {};
if (!lightSettings.patterns[patternName]) lightSettings.patterns[patternName] = {};
const patternSettings = lightSettings.patterns[patternName];
// Fall back to global colors if pattern-specific colors don't exist
const globalColors = lightSettings.colors || ['#000000'];
return {
colors: patternSettings.colors || globalColors,
delay: patternSettings.delay || lightSettings.delay || 100,
n1: patternSettings.n1 || lightSettings.n1 || 10,
n2: patternSettings.n2 || lightSettings.n2 || 10,
n3: patternSettings.n3 || lightSettings.n3 || 10,
n4: patternSettings.n4 || lightSettings.n4 || 10
};
}
async setPattern(tabName, patternName) {
const patternSettings = this.getPatternSettings(tabName, patternName);
try {
const response = await fetch('/api/pattern', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tab_name: tabName,
pattern: patternName,
delay: patternSettings.delay,
colors: patternSettings.colors,
n1: patternSettings.n1,
n2: patternSettings.n2,
n3: patternSettings.n3,
n4: patternSettings.n4
})
});
if (response.ok) {
// Update the pattern immediately in the state
if (this.state.lights[tabName]) {
this.state.lights[tabName].settings.pattern = patternName;
}
// Reload state from server to ensure consistency
await this.loadState();
// Reload tab content to update UI
await this.loadTabContent(tabName);
} else {
const errorText = await response.text();
console.error('Failed to set pattern:', errorText);
alert(`Failed to set pattern: ${errorText}`);
}
} catch (error) {
console.error('Failed to set pattern:', error);
}
}
selectColor(tabName, index, hexColor) {
this.selectedColorIndex = index;
if (this.state.lights[tabName]) {
const pattern = this.state.lights[tabName].settings.pattern;
const patternSettings = this.getPatternSettings(tabName, pattern);
this.renderColorPalette(tabName, patternSettings.colors);
}
}
async updateColorInPalette(tabName, index, hexColor) {
if (!this.currentTab || tabName !== this.currentTab) return;
const pattern = this.state.lights[tabName].settings.pattern;
const patternSettings = this.getPatternSettings(tabName, pattern);
patternSettings.colors[index] = hexColor;
// Update the preview
const palette = document.getElementById('color-palette');
if (palette && palette.children[index]) {
const preview = palette.children[index].querySelector('.color-swatch-preview');
if (preview) {
preview.style.backgroundColor = hexColor;
}
}
// Send update to backend to persist changes
const rgb = this.hexToRgb(hexColor);
try {
const response = await fetch('/api/parameters', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tab_name: this.currentTab,
red: rgb.r,
green: rgb.g,
blue: rgb.b,
color_index: index
})
});
if (response.ok) {
// Reload state to ensure persistence
await this.loadState();
// Re-render palette to reflect persisted state
const updatedPatternSettings = this.getPatternSettings(tabName, pattern);
this.renderColorPalette(tabName, updatedPatternSettings.colors);
}
} catch (error) {
console.error('Failed to update color:', error);
}
}
async updateBrightness() {
if (!this.currentTab) return;
const brightness = parseInt(document.getElementById('brightness-slider').value) || 0;
try {
await fetch('/api/parameters', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tab_name: this.currentTab,
brightness: brightness
})
});
} catch (error) {
console.error('Failed to update brightness:', error);
}
}
async updateDelay() {
if (!this.currentTab) return;
const sliderValue = parseInt(document.getElementById('delay-slider').value);
const pattern = this.state.lights[this.currentTab].settings.pattern;
const patternConfig = this.state.patterns[pattern] || {};
const minDelay = patternConfig.min_delay || 10;
const maxDelay = patternConfig.max_delay || 10000;
try {
await fetch('/api/parameters', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tab_name: this.currentTab,
delay_slider: sliderValue
})
});
} catch (error) {
console.error('Failed to update delay:', error);
}
}
async updateNParams() {
if (!this.currentTab) return;
const nParams = {};
for (let i = 1; i <= 4; i++) {
nParams[`n${i}`] = parseInt(document.getElementById(`n${i}-input`).value) || 0;
}
try {
await fetch('/api/parameters', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tab_name: this.currentTab,
...nParams
})
});
} catch (error) {
console.error('Failed to update n params:', error);
}
}
debounceUpdate(key, callback) {
if (this.updateTimeouts[key]) {
clearTimeout(this.updateTimeouts[key]);
}
this.updateTimeouts[key] = setTimeout(callback, 100);
}
delayToSlider(delayMs, minDelay = 10, maxDelay = 10000) {
if (delayMs <= minDelay) return 0;
if (delayMs >= maxDelay) return 1000;
if (minDelay === maxDelay) return 0;
return Math.floor(1000 * Math.log(delayMs / minDelay) / Math.log(maxDelay / minDelay));
}
sliderToDelay(sliderValue, minDelay = 10, maxDelay = 10000) {
if (sliderValue <= 0) return minDelay;
if (sliderValue >= 1000) return maxDelay;
if (minDelay === maxDelay) return minDelay;
return Math.floor(minDelay * Math.pow(maxDelay / minDelay, sliderValue / 1000));
}
updateDelayValue(sliderValue, minDelay, maxDelay) {
if (!minDelay || !maxDelay) {
const pattern = this.currentTab ? this.state.lights[this.currentTab]?.settings?.pattern : 'on';
const patternConfig = this.state.patterns[pattern] || {};
minDelay = patternConfig.min_delay || 10;
maxDelay = patternConfig.max_delay || 10000;
}
const delay = this.sliderToDelay(parseInt(sliderValue), minDelay, maxDelay);
document.getElementById('delay-value').textContent = `${delay} ms`;
}
hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : { r: 0, g: 0, b: 0 };
}
rgbToHex(r, g, b) {
return `#${[r, g, b].map(x => {
const hex = x.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}).join('')}`;
}
showAddTabModal() {
document.getElementById('new-tab-name').value = '';
document.getElementById('new-tab-ids').value = '1';
document.getElementById('add-tab-modal').classList.add('active');
}
async createTab() {
const name = document.getElementById('new-tab-name').value.trim();
const idsStr = document.getElementById('new-tab-ids').value.trim();
const ids = idsStr.split(',').map(id => id.trim()).filter(id => id);
if (!name) {
alert('Tab name cannot be empty');
return;
}
try {
const response = await fetch('/api/tabs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, ids })
});
if (response.ok) {
await this.loadState();
this.renderTabs();
this.selectTab(name);
this.hideModal('add-tab-modal');
} else {
const error = await response.json();
alert(error.error || 'Failed to create tab');
}
} catch (error) {
console.error('Failed to create tab:', error);
alert('Failed to create tab');
}
}
showEditTabModal() {
if (!this.currentTab) {
alert('Please select a tab first');
return;
}
const light = this.state.lights[this.currentTab];
document.getElementById('edit-tab-name').value = this.currentTab;
document.getElementById('edit-tab-ids').value = light.names.join(', ');
document.getElementById('edit-tab-modal').classList.add('active');
}
async updateTab() {
const newName = document.getElementById('edit-tab-name').value.trim();
const idsStr = document.getElementById('edit-tab-ids').value.trim();
const ids = idsStr.split(',').map(id => id.trim()).filter(id => id);
if (!newName) {
alert('Tab name cannot be empty');
return;
}
try {
const response = await fetch(`/api/tabs/${this.currentTab}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName, ids })
});
if (response.ok) {
await this.loadState();
this.renderTabs();
this.selectTab(newName);
this.hideModal('edit-tab-modal');
} else {
const error = await response.json();
alert(error.error || 'Failed to update tab');
}
} catch (error) {
console.error('Failed to update tab:', error);
alert('Failed to update tab');
}
}
async deleteCurrentTab() {
if (!this.currentTab) {
alert('Please select a tab first');
return;
}
if (!confirm(`Are you sure you want to delete the tab '${this.currentTab}'?`)) {
return;
}
try {
const response = await fetch(`/api/tabs/${this.currentTab}`, {
method: 'DELETE'
});
if (response.ok) {
await this.loadState();
this.renderTabs();
if (this.state.tab_order.length > 0) {
this.selectTab(this.state.tab_order[0]);
} else {
this.currentTab = null;
document.getElementById('tab-content').innerHTML = '<p>No tabs available. Create a new tab to get started.</p>';
}
}
} catch (error) {
console.error('Failed to delete tab:', error);
alert('Failed to delete tab');
}
}
async addColorToPalette() {
if (!this.currentTab) return;
const pattern = this.state.lights[this.currentTab].settings.pattern;
const patternSettings = this.getPatternSettings(this.currentTab, pattern);
patternSettings.colors.push('#000000');
this.selectedColorIndex = patternSettings.colors.length - 1;
this.renderColorPalette(this.currentTab, patternSettings.colors);
}
async removeSelectedColor() {
if (!this.currentTab) return;
const pattern = this.state.lights[this.currentTab].settings.pattern;
const patternSettings = this.getPatternSettings(this.currentTab, pattern);
if (patternSettings.colors.length <= 1) {
alert('There must be at least one color in the palette');
return;
}
patternSettings.colors.splice(this.selectedColorIndex, 1);
if (this.selectedColorIndex >= patternSettings.colors.length) {
this.selectedColorIndex = patternSettings.colors.length - 1;
}
this.renderColorPalette(this.currentTab, patternSettings.colors);
}
async showColorPalette() {
const modal = document.getElementById('color-palette-modal');
modal.classList.add('active');
// Update current profile display in palette modal
const profileNameDisplay = document.getElementById('palette-current-profile-name');
if (profileNameDisplay) {
profileNameDisplay.textContent = this.state.current_profile || 'None';
}
await this.loadProfilePalette();
}
async showProfiles() {
const modal = document.getElementById('profiles-modal');
modal.classList.add('active');
await this.loadProfiles();
await this.loadProfilePalette();
}
async loadProfiles() {
try {
const response = await fetch('/api/profiles');
const data = await response.json();
const profilesList = document.getElementById('profiles-list');
profilesList.innerHTML = '';
const currentProfile = data.current_profile || '';
this.state.current_profile = currentProfile;
this.state.color_palette = data.color_palette || [];
this.updateCurrentProfileDisplay();
if (data.profiles.length === 0) {
profilesList.innerHTML = '<p style="text-align: center; color: #888;">No profiles found</p>';
} else {
data.profiles.forEach(profileName => {
const profileItem = document.createElement('div');
profileItem.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 profileLabel = document.createElement('span');
profileLabel.textContent = profileName;
if (profileName === currentProfile) {
profileLabel.textContent = `${profileName}`;
profileLabel.style.fontWeight = 'bold';
profileLabel.style.color = '#FFD700';
}
const actionsContainer = document.createElement('div');
actionsContainer.style.cssText = 'display: flex; gap: 0.5rem;';
const loadButton = document.createElement('button');
loadButton.className = 'btn btn-small';
loadButton.textContent = 'Load';
loadButton.addEventListener('click', () => this.loadProfile(profileName));
const deleteButton = document.createElement('button');
deleteButton.className = 'btn btn-small btn-danger';
deleteButton.textContent = 'Delete';
deleteButton.addEventListener('click', () => this.deleteProfile(profileName));
actionsContainer.appendChild(loadButton);
actionsContainer.appendChild(deleteButton);
profileItem.appendChild(profileLabel);
profileItem.appendChild(actionsContainer);
profilesList.appendChild(profileItem);
});
}
} catch (error) {
console.error('Failed to load profiles:', error);
alert('Failed to load profiles');
}
}
async deleteProfile(profileName) {
if (!confirm(`Delete profile '${profileName}'? This cannot be undone.`)) {
return;
}
try {
const response = await fetch(`/api/profiles/${profileName}`, {
method: 'DELETE'
});
if (response.ok) {
await this.loadProfiles();
// If the current profile was deleted, clear current state tabs
if (this.state.current_profile === profileName) {
this.state.current_profile = '';
this.state.lights = {};
this.state.tab_order = [];
this.renderTabs();
document.getElementById('tab-content').innerHTML = '<p>No tabs available. Create a new tab to get started.</p>';
this.updateCurrentProfileDisplay();
}
} else {
const error = await response.json();
alert(error.error || 'Failed to delete profile');
}
} catch (error) {
console.error('Failed to delete profile:', error);
alert('Failed to delete profile');
}
}
async loadProfile(profileName) {
try {
const response = await fetch(`/api/profiles/${profileName}`, {
method: 'POST'
});
if (response.ok) {
await this.loadState();
this.renderTabs();
if (this.state.tab_order.length > 0) {
this.selectTab(this.state.tab_order[0]);
} else {
this.currentTab = null;
}
await this.loadProfiles(); // Refresh the profiles list
} else {
const error = await response.json();
alert(error.error || 'Failed to load profile');
}
} catch (error) {
console.error('Failed to load profile:', error);
alert('Failed to load profile');
}
}
async createProfile() {
const nameInput = document.getElementById('new-profile-name');
const profileName = nameInput.value.trim();
if (!profileName) {
alert('Profile name cannot be empty');
return;
}
try {
const response = await fetch('/api/profiles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: profileName })
});
if (response.ok) {
nameInput.value = '';
await this.loadProfiles();
alert(`Profile '${profileName}' created successfully`);
} else {
const error = await response.json();
alert(error.error || 'Failed to create profile');
}
} catch (error) {
console.error('Failed to create profile:', error);
alert('Failed to create profile');
}
}
async loadProfilePalette() {
const currentProfile = this.state.current_profile;
if (!currentProfile) {
this.renderProfilePalette([]);
return;
}
try {
const response = await fetch(`/api/profiles/${currentProfile}/palette`);
if (response.ok) {
const data = await response.json();
this.renderProfilePalette(data.color_palette || []);
} else {
this.renderProfilePalette([]);
}
} catch (error) {
console.error('Failed to load profile palette:', error);
this.renderProfilePalette([]);
}
}
renderProfilePalette(colors) {
// Render in profiles modal
const container = document.getElementById('profile-palette-container');
if (container) {
container.innerHTML = '';
colors.forEach((color, index) => {
const swatch = this.createPaletteSwatch(color, index);
container.appendChild(swatch);
});
}
// Render in color palette modal
const paletteContainer = document.getElementById('palette-container');
if (paletteContainer) {
paletteContainer.innerHTML = '';
colors.forEach((color, index) => {
const swatch = this.createPaletteSwatch(color, index);
paletteContainer.appendChild(swatch);
});
}
}
createPaletteSwatch(color, index) {
const swatch = document.createElement('div');
swatch.style.cssText = 'width: 40px; height: 40px; background-color: ' + color + '; border: 2px solid #4a4a4a; border-radius: 4px; cursor: pointer; position: relative;';
swatch.title = `Click to apply ${color} to selected color`;
// Click to apply color to currently selected color in active tab
swatch.addEventListener('click', (e) => {
// Only apply if not clicking the remove button
if (e.target === swatch || !e.target.closest('button')) {
this.applyPaletteColorToSelected(color);
}
});
const removeBtn = document.createElement('button');
removeBtn.textContent = '×';
removeBtn.style.cssText = 'position: absolute; top: -8px; right: -8px; width: 20px; height: 20px; border-radius: 50%; background-color: #ff4444; color: white; border: none; cursor: pointer; font-size: 14px; line-height: 1; display: flex; align-items: center; justify-content: center; z-index: 10;';
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.removePaletteColor(index);
});
swatch.appendChild(removeBtn);
return swatch;
}
applyPaletteColorToSelected(paletteColor) {
if (!this.currentTab) {
alert('No tab selected. Please select a tab first.');
return;
}
const pattern = this.state.lights[this.currentTab].settings.pattern;
const patternSettings = this.getPatternSettings(this.currentTab, pattern);
if (patternSettings.colors.length === 0) {
alert('No colors in the current pattern. Add a color first.');
return;
}
// Apply the palette color to the currently selected color index
const selectedIndex = this.selectedColorIndex;
if (selectedIndex >= 0 && selectedIndex < patternSettings.colors.length) {
this.updateColorInPalette(this.currentTab, selectedIndex, paletteColor);
} else {
// If no color is selected, apply to the first color
this.updateColorInPalette(this.currentTab, 0, paletteColor);
}
}
async addPaletteColor() {
const colorInput = document.getElementById('new-palette-color');
if (!colorInput) return;
await this.addPaletteColorFromInput(colorInput);
}
async addPaletteColorFromModal() {
const colorInput = document.getElementById('palette-new-color');
if (!colorInput) return;
await this.addPaletteColorFromInput(colorInput);
}
async addPaletteColorFromInput(colorInput) {
const color = colorInput.value;
const currentProfile = this.state.current_profile;
if (!currentProfile) {
alert('No profile selected');
return;
}
try {
const currentPalette = this.state.color_palette || [];
if (currentPalette.includes(color)) {
alert('Color already in palette');
return;
}
const newPalette = [...currentPalette, color];
const response = await fetch(`/api/profiles/${currentProfile}/palette`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ color_palette: newPalette })
});
if (response.ok) {
this.state.color_palette = newPalette;
await this.loadProfilePalette();
} else {
alert('Failed to add color to palette');
}
} catch (error) {
console.error('Failed to add palette color:', error);
alert('Failed to add color to palette');
}
}
async removePaletteColor(index) {
const currentProfile = this.state.current_profile;
if (!currentProfile) {
alert('No profile selected');
return;
}
try {
const currentPalette = this.state.color_palette || [];
const newPalette = currentPalette.filter((_, i) => i !== index);
const response = await fetch(`/api/profiles/${currentProfile}/palette`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ color_palette: newPalette })
});
if (response.ok) {
this.state.color_palette = newPalette;
await this.loadProfilePalette();
} else {
alert('Failed to remove color from palette');
}
} catch (error) {
console.error('Failed to remove palette color:', error);
alert('Failed to remove color from palette');
}
}
showPaletteQuickSelect(wrapper, tabName, colorIndex, colorPickerInput) {
const palette = this.state.color_palette || [];
if (palette.length === 0) {
// No palette colors, allow native picker to open
return;
}
// Store context for the modal
this.quickPaletteContext = {
tabName: tabName,
colorIndex: colorIndex,
colorPickerInput: colorPickerInput
};
// Show the modal
const modal = document.getElementById('quick-palette-modal');
modal.classList.add('active');
// Render palette colors in the modal
this.renderQuickPalette(palette);
}
renderQuickPalette(palette) {
const container = document.getElementById('quick-palette-container');
if (!container) return;
container.innerHTML = '';
if (palette.length === 0) {
container.innerHTML = '<p style="text-align: center; color: #888; width: 100%;">No colors in palette</p>';
return;
}
palette.forEach((color) => {
const colorBtn = document.createElement('div');
colorBtn.style.cssText = `width: 80px; height: 80px; background-color: ${color}; border: 3px solid #4a4a4a; border-radius: 8px; cursor: pointer; flex-shrink: 0; position: relative; transition: transform 0.2s, border-color 0.2s;`;
colorBtn.title = color;
// Add color hex label
const label = document.createElement('div');
label.style.cssText = 'position: absolute; bottom: -20px; left: 0; right: 0; text-align: center; font-size: 0.7rem; color: #aaa;';
label.textContent = color;
colorBtn.appendChild(label);
colorBtn.addEventListener('mouseenter', () => {
colorBtn.style.transform = 'scale(1.1)';
colorBtn.style.borderColor = '#6a5acd';
});
colorBtn.addEventListener('mouseleave', () => {
colorBtn.style.transform = 'scale(1)';
colorBtn.style.borderColor = '#4a4a4a';
});
colorBtn.addEventListener('click', () => {
if (this.quickPaletteContext) {
const { tabName, colorIndex, colorPickerInput } = this.quickPaletteContext;
this.updateColorInPalette(tabName, colorIndex, color);
colorPickerInput.value = color;
}
this.hideQuickPaletteModal();
});
container.appendChild(colorBtn);
});
}
hideQuickPaletteModal() {
const modal = document.getElementById('quick-palette-modal');
modal.classList.remove('active');
this.quickPaletteContext = null;
}
useColorPickerFromQuickPalette() {
if (this.quickPaletteContext && this.quickPaletteContext.colorPickerInput) {
const colorPickerInput = this.quickPaletteContext.colorPickerInput;
// Mark that we want to allow native picker
colorPickerInput.dataset.allowNative = 'true';
// Temporarily remove the click handler
if (colorPickerInput._clickHandler) {
colorPickerInput.removeEventListener('click', colorPickerInput._clickHandler);
}
this.hideQuickPaletteModal();
// Trigger native color picker after closing modal
setTimeout(() => {
// Try using showPicker() if available (modern browsers)
if (colorPickerInput.showPicker) {
colorPickerInput.showPicker().catch(() => {
// Fallback to click if showPicker fails
colorPickerInput.click();
});
} else {
// Fallback to click for older browsers
colorPickerInput.click();
}
// Restore the click handler after a moment
setTimeout(() => {
if (colorPickerInput._clickHandler) {
colorPickerInput.addEventListener('click', colorPickerInput._clickHandler);
}
delete colorPickerInput.dataset.allowNative;
}, 500);
}, 200);
}
}
hideModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
}
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new LightingController();
});