Add profile color palette feature with quick-select modal
- Added per-profile color palette storage in profile JSON files - Created Color Palette modal for managing profile colors - Added quick-select modal window when clicking color pickers - Implemented palette color selection to apply to active tab colors - Added 'Use Color Picker' button in quick palette modal - Fixed pattern selection to properly update UI - Improved color picker interaction to prevent conflicts between quick palette and native picker
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"tab_password": "",
|
"tab_password": "",
|
||||||
"lights": {
|
"lights": {
|
||||||
"test": {
|
"dsfdfd": {
|
||||||
"names": [
|
"names": [
|
||||||
"1"
|
"1"
|
||||||
],
|
],
|
||||||
@@ -21,6 +21,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tab_order": [
|
"tab_order": [
|
||||||
"test"
|
"dsfdfd"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"tab_password": "qwerty1234",
|
||||||
"lights": {
|
"lights": {
|
||||||
"sign": {
|
"sign": {
|
||||||
"names": [
|
"names": [
|
||||||
@@ -846,7 +847,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tab_password": "qwerty1234",
|
|
||||||
"tab_order": [
|
"tab_order": [
|
||||||
"sign",
|
"sign",
|
||||||
"dj",
|
"dj",
|
||||||
@@ -860,5 +860,9 @@
|
|||||||
"front1",
|
"front1",
|
||||||
"front2",
|
"front2",
|
||||||
"front3"
|
"front3"
|
||||||
|
],
|
||||||
|
"color_palette": [
|
||||||
|
"#c33232",
|
||||||
|
"#3237c3"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"tab_password": "",
|
"tab_password": "",
|
||||||
"current_profile": "ring",
|
"current_profile": "tt",
|
||||||
"patterns": {
|
"patterns": {
|
||||||
"on": {
|
"on": {
|
||||||
"min_delay": 10,
|
"min_delay": 10,
|
||||||
|
|||||||
131
src/flask_app.py
131
src/flask_app.py
@@ -22,6 +22,32 @@ settings = Settings()
|
|||||||
websocket_client = None
|
websocket_client = None
|
||||||
websocket_uri = "ws://192.168.4.1:80/ws"
|
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):
|
def delay_to_slider(delay_ms, min_delay=10, max_delay=10000):
|
||||||
"""Convert delay in ms to slider position (0-1000) using logarithmic scale."""
|
"""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] = {}
|
light_settings["patterns"][pattern_name] = {}
|
||||||
|
|
||||||
pattern_settings = 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 {
|
return {
|
||||||
"colors": pattern_settings.get("colors", ["#000000"]),
|
"colors": pattern_settings.get("colors", global_colors),
|
||||||
"delay": pattern_settings.get("delay", 100),
|
"delay": pattern_settings.get("delay", light_settings.get("delay", 100)),
|
||||||
"n1": pattern_settings.get("n1", 10),
|
"n1": pattern_settings.get("n1", light_settings.get("n1", 10)),
|
||||||
"n2": pattern_settings.get("n2", 10),
|
"n2": pattern_settings.get("n2", light_settings.get("n2", 10)),
|
||||||
"n3": pattern_settings.get("n3", 10),
|
"n3": pattern_settings.get("n3", light_settings.get("n3", 10)),
|
||||||
"n4": pattern_settings.get("n4", 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():
|
def save_current_profile():
|
||||||
"""Save current settings to the active profile file."""
|
"""Save current settings to the active profile file."""
|
||||||
current_profile = settings.get("current_profile")
|
current_profile = settings.get("current_profile")
|
||||||
|
|
||||||
|
# If no profile is set, create/use a default profile
|
||||||
if not current_profile:
|
if not current_profile:
|
||||||
return
|
current_profile = "default"
|
||||||
|
settings["current_profile"] = current_profile
|
||||||
|
# Save current_profile to settings.json
|
||||||
|
settings.save()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
profiles_dir = "profiles"
|
profiles_dir = "profiles"
|
||||||
@@ -150,10 +183,30 @@ def index():
|
|||||||
@app.route('/api/state', methods=['GET'])
|
@app.route('/api/state', methods=['GET'])
|
||||||
def get_state():
|
def get_state():
|
||||||
"""Get the current state of all lights."""
|
"""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({
|
return jsonify({
|
||||||
"lights": settings.get("lights", {}),
|
"lights": settings.get("lights", {}),
|
||||||
"patterns": settings.get("patterns", {}),
|
"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({
|
return jsonify({
|
||||||
"profiles": profiles,
|
"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 = {
|
empty_profile = {
|
||||||
"lights": {},
|
"lights": {},
|
||||||
"tab_password": "",
|
"tab_password": "",
|
||||||
"tab_order": []
|
"tab_order": [],
|
||||||
|
"color_palette": []
|
||||||
}
|
}
|
||||||
|
|
||||||
with open(profile_path, 'w') as file:
|
with open(profile_path, 'w') as file:
|
||||||
@@ -459,6 +514,9 @@ def load_profile(profile_name):
|
|||||||
settings.update(profile_data)
|
settings.update(profile_data)
|
||||||
settings["patterns"] = patterns_backup
|
settings["patterns"] = patterns_backup
|
||||||
settings["current_profile"] = profile_name
|
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 = {
|
settings_to_save = {
|
||||||
"tab_password": tab_password_backup,
|
"tab_password": tab_password_backup,
|
||||||
@@ -470,6 +528,59 @@ def load_profile(profile_name):
|
|||||||
|
|
||||||
return jsonify({"success": True})
|
return jsonify({"success": True})
|
||||||
|
|
||||||
|
@app.route('/api/profiles/<profile_name>/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/<profile_name>/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/<profile_name>/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():
|
def init_websocket():
|
||||||
"""Initialize WebSocket connection in background."""
|
"""Initialize WebSocket connection in background."""
|
||||||
|
|||||||
483
static/app.js
483
static/app.js
@@ -9,6 +9,7 @@ class LightingController {
|
|||||||
};
|
};
|
||||||
this.selectedColorIndex = 0;
|
this.selectedColorIndex = 0;
|
||||||
this.updateTimeouts = {};
|
this.updateTimeouts = {};
|
||||||
|
this.quickPaletteContext = null;
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
@@ -27,16 +28,32 @@ class LightingController {
|
|||||||
const response = await fetch('/api/state');
|
const response = await fetch('/api/state');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
this.state = data;
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to load state:', 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() {
|
setupEventListeners() {
|
||||||
// Tab management
|
// Tab management
|
||||||
document.getElementById('add-tab-btn').addEventListener('click', () => this.showAddTabModal());
|
document.getElementById('add-tab-btn').addEventListener('click', () => this.showAddTabModal());
|
||||||
document.getElementById('edit-tab-btn').addEventListener('click', () => this.showEditTabModal());
|
document.getElementById('edit-tab-btn').addEventListener('click', () => this.showEditTabModal());
|
||||||
document.getElementById('delete-tab-btn').addEventListener('click', () => this.deleteCurrentTab());
|
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());
|
document.getElementById('profiles-btn').addEventListener('click', () => this.showProfiles());
|
||||||
|
|
||||||
// Modal actions
|
// Modal actions
|
||||||
@@ -44,6 +61,20 @@ class LightingController {
|
|||||||
document.getElementById('add-tab-cancel').addEventListener('click', () => this.hideModal('add-tab-modal'));
|
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-confirm').addEventListener('click', () => this.updateTab());
|
||||||
document.getElementById('edit-tab-cancel').addEventListener('click', () => this.hideModal('edit-tab-modal'));
|
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
|
// Brightness and delay sliders
|
||||||
document.getElementById('brightness-slider').addEventListener('input', (e) => {
|
document.getElementById('brightness-slider').addEventListener('input', (e) => {
|
||||||
@@ -73,6 +104,9 @@ class LightingController {
|
|||||||
document.getElementById('edit-tab-modal').addEventListener('click', (e) => {
|
document.getElementById('edit-tab-modal').addEventListener('click', (e) => {
|
||||||
if (e.target.id === 'edit-tab-modal') this.hideModal('edit-tab-modal');
|
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() {
|
renderTabs() {
|
||||||
@@ -113,7 +147,9 @@ class LightingController {
|
|||||||
// Update IDs display
|
// Update IDs display
|
||||||
document.getElementById('current-ids').textContent = light.names.join(', ');
|
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
|
// Update brightness slider
|
||||||
const brightness = settings.brightness || 127;
|
const brightness = settings.brightness || 127;
|
||||||
@@ -135,9 +171,6 @@ class LightingController {
|
|||||||
|
|
||||||
// Render patterns
|
// Render patterns
|
||||||
this.renderPatterns(tabName, pattern);
|
this.renderPatterns(tabName, pattern);
|
||||||
|
|
||||||
// Render color palette
|
|
||||||
this.renderColorPalette(tabName, colors);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPatterns(tabName, activePattern) {
|
renderPatterns(tabName, activePattern) {
|
||||||
@@ -178,19 +211,41 @@ class LightingController {
|
|||||||
label.className = 'color-swatch-label';
|
label.className = 'color-swatch-label';
|
||||||
label.textContent = `Color ${index + 1}`;
|
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');
|
const colorPicker = document.createElement('input');
|
||||||
colorPicker.type = 'color';
|
colorPicker.type = 'color';
|
||||||
colorPicker.value = hexColor;
|
colorPicker.value = hexColor;
|
||||||
colorPicker.className = 'color-picker-input';
|
colorPicker.className = 'color-picker-input';
|
||||||
|
colorPicker.dataset.tabName = tabName;
|
||||||
|
colorPicker.dataset.colorIndex = index;
|
||||||
colorPicker.addEventListener('change', (e) => {
|
colorPicker.addEventListener('change', (e) => {
|
||||||
const newColor = e.target.value;
|
const newColor = e.target.value;
|
||||||
this.updateColorInPalette(tabName, index, newColor);
|
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(preview);
|
||||||
swatch.appendChild(label);
|
swatch.appendChild(label);
|
||||||
swatch.appendChild(colorPicker);
|
swatch.appendChild(colorPickerWrapper);
|
||||||
swatch.addEventListener('click', (e) => {
|
swatch.addEventListener('click', (e) => {
|
||||||
// Don't trigger selection if clicking on the color picker
|
// Don't trigger selection if clicking on the color picker
|
||||||
if (e.target !== colorPicker && !colorPicker.contains(e.target)) {
|
if (e.target !== colorPicker && !colorPicker.contains(e.target)) {
|
||||||
@@ -211,13 +266,15 @@ class LightingController {
|
|||||||
if (!lightSettings.patterns[patternName]) lightSettings.patterns[patternName] = {};
|
if (!lightSettings.patterns[patternName]) lightSettings.patterns[patternName] = {};
|
||||||
|
|
||||||
const patternSettings = 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 {
|
return {
|
||||||
colors: patternSettings.colors || ['#000000'],
|
colors: patternSettings.colors || globalColors,
|
||||||
delay: patternSettings.delay || 100,
|
delay: patternSettings.delay || lightSettings.delay || 100,
|
||||||
n1: patternSettings.n1 || 10,
|
n1: patternSettings.n1 || lightSettings.n1 || 10,
|
||||||
n2: patternSettings.n2 || 10,
|
n2: patternSettings.n2 || lightSettings.n2 || 10,
|
||||||
n3: patternSettings.n3 || 10,
|
n3: patternSettings.n3 || lightSettings.n3 || 10,
|
||||||
n4: patternSettings.n4 || 10
|
n4: patternSettings.n4 || lightSettings.n4 || 10
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,8 +298,18 @@ class LightingController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
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();
|
await this.loadState();
|
||||||
|
// Reload tab content to update UI
|
||||||
await this.loadTabContent(tabName);
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to set pattern:', error);
|
console.error('Failed to set pattern:', error);
|
||||||
@@ -555,8 +622,396 @@ class LightingController {
|
|||||||
this.renderColorPalette(this.currentTab, patternSettings.colors);
|
this.renderColorPalette(this.currentTab, patternSettings.colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
showProfiles() {
|
async showColorPalette() {
|
||||||
alert('Profiles feature coming soon');
|
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 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 = '<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) {
|
hideModal(modalId) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<button id="add-tab-btn" class="btn btn-primary">+ Add Tab</button>
|
<button id="add-tab-btn" class="btn btn-primary">+ Add Tab</button>
|
||||||
<button id="edit-tab-btn" class="btn btn-secondary">Edit Tab</button>
|
<button id="edit-tab-btn" class="btn btn-secondary">Edit Tab</button>
|
||||||
<button id="delete-tab-btn" class="btn btn-danger">Delete Tab</button>
|
<button id="delete-tab-btn" class="btn btn-danger">Delete Tab</button>
|
||||||
|
<button id="color-palette-btn" class="btn btn-secondary">Color Palette</button>
|
||||||
<button id="profiles-btn" class="btn btn-secondary">Profiles</button>
|
<button id="profiles-btn" class="btn btn-secondary">Profiles</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -114,6 +115,79 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="profiles-modal" class="modal">
|
||||||
|
<div class="modal-content" style="min-width: 500px;">
|
||||||
|
<h2>Profiles</h2>
|
||||||
|
<div id="profiles-list-container" style="margin: 1rem 0; max-height: 400px; overflow-y: auto;">
|
||||||
|
<div id="profiles-list"></div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 1rem;">
|
||||||
|
<label>New Profile Name:</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
<input type="text" id="new-profile-name" placeholder="Enter profile name" style="flex: 1;">
|
||||||
|
<button id="create-profile-btn" class="btn btn-primary">Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 1rem;">
|
||||||
|
<label>Current Profile:</label>
|
||||||
|
<div id="current-profile-display" style="padding: 0.5rem; background-color: #3a3a3a; border-radius: 4px; margin-top: 0.5rem;">
|
||||||
|
<span id="current-profile-name">None</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 1.5rem;">
|
||||||
|
<label>Profile Color Palette:</label>
|
||||||
|
<div id="profile-palette-container" style="margin-top: 0.5rem; display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0.5rem; background-color: #3a3a3a; border-radius: 4px; min-height: 60px;">
|
||||||
|
<!-- Palette colors will be rendered here -->
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
<input type="color" id="new-palette-color" value="#000000" style="width: 60px; height: 40px; border: 1px solid #4a4a4a; border-radius: 4px; cursor: pointer;">
|
||||||
|
<button id="add-palette-color-btn" class="btn btn-small">Add to Palette</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="profiles-close-btn" class="btn btn-secondary">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="color-palette-modal" class="modal">
|
||||||
|
<div class="modal-content" style="min-width: 500px;">
|
||||||
|
<h2>Color Palette</h2>
|
||||||
|
<div style="margin-top: 1rem;">
|
||||||
|
<label>Current Profile:</label>
|
||||||
|
<div id="palette-current-profile-display" style="padding: 0.5rem; background-color: #3a3a3a; border-radius: 4px; margin-top: 0.5rem;">
|
||||||
|
<span id="palette-current-profile-name">None</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 1.5rem;">
|
||||||
|
<label>Profile Color Palette:</label>
|
||||||
|
<div id="palette-container" style="margin-top: 0.5rem; display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0.5rem; background-color: #3a3a3a; border-radius: 4px; min-height: 60px;">
|
||||||
|
<!-- Palette colors will be rendered here -->
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
<input type="color" id="palette-new-color" value="#000000" style="width: 60px; height: 40px; border: 1px solid #4a4a4a; border-radius: 4px; cursor: pointer;">
|
||||||
|
<button id="palette-add-color-btn" class="btn btn-small">Add to Palette</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="color-palette-close-btn" class="btn btn-secondary">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="quick-palette-modal" class="modal">
|
||||||
|
<div class="modal-content" style="min-width: 500px; max-width: 600px;">
|
||||||
|
<h2>Select Color from Palette</h2>
|
||||||
|
<div id="quick-palette-container" style="margin-top: 1rem; display: flex; flex-wrap: wrap; gap: 0.75rem; padding: 1rem; background-color: #3a3a3a; border-radius: 4px; min-height: 200px; max-height: 500px; overflow-y: auto;">
|
||||||
|
<!-- Palette colors will be rendered here -->
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions" style="margin-top: 1rem;">
|
||||||
|
<button id="quick-palette-use-picker-btn" class="btn btn-secondary">Use Color Picker</button>
|
||||||
|
<button id="quick-palette-close-btn" class="btn btn-secondary">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user