diff --git a/profiles/ring.json b/profiles/ring.json index 4d8f86e..5fc2f12 100644 --- a/profiles/ring.json +++ b/profiles/ring.json @@ -1,7 +1,7 @@ { "tab_password": "", "lights": { - "test": { + "dsfdfd": { "names": [ "1" ], @@ -21,6 +21,6 @@ } }, "tab_order": [ - "test" + "dsfdfd" ] } \ No newline at end of file diff --git a/profiles/tt.json b/profiles/tt.json index 7628840..7598bd2 100644 --- a/profiles/tt.json +++ b/profiles/tt.json @@ -1,4 +1,5 @@ { + "tab_password": "qwerty1234", "lights": { "sign": { "names": [ @@ -846,7 +847,6 @@ } } }, - "tab_password": "qwerty1234", "tab_order": [ "sign", "dj", @@ -860,5 +860,9 @@ "front1", "front2", "front3" + ], + "color_palette": [ + "#c33232", + "#3237c3" ] } \ No newline at end of file diff --git a/settings.json b/settings.json index f67b21d..5a1c660 100644 --- a/settings.json +++ b/settings.json @@ -1,6 +1,6 @@ { "tab_password": "", - "current_profile": "ring", + "current_profile": "tt", "patterns": { "on": { "min_delay": 10, diff --git a/src/flask_app.py b/src/flask_app.py index d48ddd3..d479f91 100644 --- a/src/flask_app.py +++ b/src/flask_app.py @@ -22,6 +22,32 @@ settings = Settings() websocket_client = None websocket_uri = "ws://192.168.4.1:80/ws" +# Load current profile on startup +def load_current_profile(): + """Load the current profile if one is set.""" + current_profile = settings.get("current_profile") + if current_profile: + profile_path = os.path.join("profiles", f"{current_profile}.json") + if os.path.exists(profile_path): + try: + with open(profile_path, 'r') as file: + profile_data = json.load(file) + + # Update settings with profile data + profile_data.pop("current_profile", None) + patterns_backup = settings.get("patterns", {}) + tab_password_backup = settings.get("tab_password", "") + + settings.update(profile_data) + settings["patterns"] = patterns_backup + settings["current_profile"] = current_profile + print(f"Loaded profile '{current_profile}' on startup.") + except Exception as e: + print(f"Error loading profile '{current_profile}': {e}") + +# Load current profile when module is imported +load_current_profile() + def delay_to_slider(delay_ms, min_delay=10, max_delay=10000): """Convert delay in ms to slider position (0-1000) using logarithmic scale.""" @@ -54,13 +80,15 @@ def get_pattern_settings(tab_name, pattern_name): light_settings["patterns"][pattern_name] = {} pattern_settings = light_settings["patterns"][pattern_name] + # Fall back to global settings if pattern-specific settings don't exist + global_colors = light_settings.get("colors", ["#000000"]) return { - "colors": pattern_settings.get("colors", ["#000000"]), - "delay": pattern_settings.get("delay", 100), - "n1": pattern_settings.get("n1", 10), - "n2": pattern_settings.get("n2", 10), - "n3": pattern_settings.get("n3", 10), - "n4": pattern_settings.get("n4", 10), + "colors": pattern_settings.get("colors", global_colors), + "delay": pattern_settings.get("delay", light_settings.get("delay", 100)), + "n1": pattern_settings.get("n1", light_settings.get("n1", 10)), + "n2": pattern_settings.get("n2", light_settings.get("n2", 10)), + "n3": pattern_settings.get("n3", light_settings.get("n3", 10)), + "n4": pattern_settings.get("n4", light_settings.get("n4", 10)), } @@ -86,8 +114,13 @@ def save_pattern_settings(tab_name, pattern_name, colors=None, delay=None, n_par def save_current_profile(): """Save current settings to the active profile file.""" current_profile = settings.get("current_profile") + + # If no profile is set, create/use a default profile if not current_profile: - return + current_profile = "default" + settings["current_profile"] = current_profile + # Save current_profile to settings.json + settings.save() try: profiles_dir = "profiles" @@ -150,10 +183,30 @@ def index(): @app.route('/api/state', methods=['GET']) def get_state(): """Get the current state of all lights.""" + # Ensure a profile is set if we have lights but no profile + if settings.get("lights") and not settings.get("current_profile"): + current_profile = "default" + settings["current_profile"] = current_profile + settings.save() + # Create default profile file if it doesn't exist + profiles_dir = "profiles" + os.makedirs(profiles_dir, exist_ok=True) + profile_path = os.path.join(profiles_dir, f"{current_profile}.json") + if not os.path.exists(profile_path): + profile_data = { + "lights": settings.get("lights", {}), + "tab_order": settings.get("tab_order", []), + "tab_password": settings.get("tab_password", "") + } + with open(profile_path, 'w') as file: + json.dump(profile_data, file, indent=4) + return jsonify({ "lights": settings.get("lights", {}), "patterns": settings.get("patterns", {}), - "tab_order": settings.get("tab_order", []) + "tab_order": settings.get("tab_order", []), + "current_profile": settings.get("current_profile", ""), + "color_palette": settings.get("color_palette", []) }) @@ -408,7 +461,8 @@ def get_profiles(): return jsonify({ "profiles": profiles, - "current_profile": settings.get("current_profile", "") + "current_profile": settings.get("current_profile", ""), + "color_palette": settings.get("color_palette", []) }) @@ -431,7 +485,8 @@ def create_profile(): empty_profile = { "lights": {}, "tab_password": "", - "tab_order": [] + "tab_order": [], + "color_palette": [] } with open(profile_path, 'w') as file: @@ -459,6 +514,9 @@ def load_profile(profile_name): settings.update(profile_data) settings["patterns"] = patterns_backup settings["current_profile"] = profile_name + # Ensure color_palette exists (default to empty array if not in profile) + if "color_palette" not in settings: + settings["color_palette"] = [] settings_to_save = { "tab_password": tab_password_backup, @@ -470,6 +528,59 @@ def load_profile(profile_name): return jsonify({"success": True}) +@app.route('/api/profiles//palette', methods=['GET']) +def get_profile_palette(profile_name): + """Get the color palette for a profile.""" + profile_path = os.path.join("profiles", f"{profile_name}.json") + + if not os.path.exists(profile_path): + return jsonify({"error": f"Profile '{profile_name}' not found"}), 404 + + with open(profile_path, 'r') as file: + profile_data = json.load(file) + + palette = profile_data.get("color_palette", []) + return jsonify({"color_palette": palette}) + +@app.route('/api/profiles//palette', methods=['POST']) +def update_profile_palette(profile_name): + """Update the color palette for a profile.""" + data = request.json + color_palette = data.get("color_palette", []) + + profile_path = os.path.join("profiles", f"{profile_name}.json") + + if not os.path.exists(profile_path): + return jsonify({"error": f"Profile '{profile_name}' not found"}), 404 + + with open(profile_path, 'r') as file: + profile_data = json.load(file) + + profile_data["color_palette"] = color_palette + + with open(profile_path, 'w') as file: + json.dump(profile_data, file, indent=4) + + # Update current settings if this is the active profile + if settings.get("current_profile") == profile_name: + settings["color_palette"] = color_palette + + return jsonify({"success": True, "color_palette": color_palette}) + +@app.route('/api/profiles//save', methods=['POST']) +def save_profile(profile_name): + """Save current state to a profile.""" + # Save current state to the specified profile + save_current_profile() + + # If saving to a different profile, switch to it + if profile_name != settings.get("current_profile"): + settings["current_profile"] = profile_name + settings.save() + save_current_profile() + + return jsonify({"success": True}) + def init_websocket(): """Initialize WebSocket connection in background.""" diff --git a/static/app.js b/static/app.js index 117a897..b059d75 100644 --- a/static/app.js +++ b/static/app.js @@ -9,6 +9,7 @@ class LightingController { }; this.selectedColorIndex = 0; this.updateTimeouts = {}; + this.quickPaletteContext = null; this.init(); } @@ -27,16 +28,32 @@ class LightingController { 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 @@ -44,6 +61,20 @@ class LightingController { 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) => { @@ -73,6 +104,9 @@ class LightingController { 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() { @@ -113,7 +147,9 @@ class LightingController { // Update IDs display document.getElementById('current-ids').textContent = light.names.join(', '); - // Colors are handled by the color palette with individual color pickers + // Load and render colors in the palette + const colors = patternSettings.colors || ['#000000']; + this.renderColorPalette(tabName, colors); // Update brightness slider const brightness = settings.brightness || 127; @@ -135,9 +171,6 @@ class LightingController { // Render patterns this.renderPatterns(tabName, pattern); - - // Render color palette - this.renderColorPalette(tabName, colors); } renderPatterns(tabName, activePattern) { @@ -178,19 +211,41 @@ class LightingController { label.className = 'color-swatch-label'; label.textContent = `Color ${index + 1}`; - // Color picker input + // 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(colorPicker); + 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)) { @@ -211,13 +266,15 @@ class LightingController { 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 || ['#000000'], - delay: patternSettings.delay || 100, - n1: patternSettings.n1 || 10, - n2: patternSettings.n2 || 10, - n3: patternSettings.n3 || 10, - n4: patternSettings.n4 || 10 + 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 }; } @@ -241,8 +298,18 @@ class LightingController { }); 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); @@ -555,8 +622,396 @@ class LightingController { this.renderColorPalette(this.currentTab, patternSettings.colors); } - showProfiles() { - alert('Profiles feature coming soon'); + 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 = '

No profiles found

'; + } 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 loadButton = document.createElement('button'); + loadButton.className = 'btn btn-small'; + loadButton.textContent = 'Load'; + loadButton.addEventListener('click', () => this.loadProfile(profileName)); + + profileItem.appendChild(profileLabel); + profileItem.appendChild(loadButton); + profilesList.appendChild(profileItem); + }); + } + } catch (error) { + console.error('Failed to load profiles:', error); + alert('Failed to load profiles'); + } + } + + 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 = '

No colors in palette

'; + 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) { diff --git a/templates/index.html b/templates/index.html index ff7905a..0753ae8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -14,6 +14,7 @@ + @@ -114,6 +115,79 @@ + + + + + +