1 Commits

Author SHA1 Message Date
58806ef654 Add config for Kat 2025-09-13 16:25:33 +12:00
16 changed files with 178 additions and 8168 deletions

View File

@@ -12,8 +12,6 @@ python-rtmidi = "*"
pyaudio = "*"
aubio = "*"
websocket-client = "*"
flask = "*"
flask-cors = "*"
[dev-packages]
@@ -21,6 +19,5 @@ flask-cors = "*"
python_version = "3.12"
[scripts]
main = "python src/main.py"
main = "python main.py"
dev = 'watchfiles "python src/main.py" src'
web = "python run_web.py"

View File

@@ -1,72 +0,0 @@
{
"tab_password": "",
"lights": {
"test": {
"names": [
"test"
],
"settings": {
"pattern": "transition",
"brightness": 127,
"colors": [
"#000000"
],
"delay": 100,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"patterns": {
"on": {
"colors": [
"#000000"
],
"delay": 100,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"off": {
"colors": [
"#000000"
],
"delay": 100,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"rainbow": {
"colors": [
"#000000"
],
"delay": 100,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"transition": {
"colors": [
"#c12525",
"#246dcc"
],
"delay": 1321,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
}
}
}
}
},
"tab_order": [
"test"
],
"color_palette": [
"#c12525",
"#246dcc"
]
}

View File

@@ -1,26 +0,0 @@
{
"tab_password": "",
"lights": {
"dsfdfd": {
"names": [
"1"
],
"settings": {
"pattern": "on",
"brightness": 127,
"colors": [
"#000000"
],
"delay": 100,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"patterns": {}
}
}
},
"tab_order": [
"dsfdfd"
]
}

View File

@@ -1,6 +0,0 @@
{
"lights": {},
"tab_password": "",
"tab_order": [],
"color_palette": []
}

View File

@@ -1,868 +0,0 @@
{
"tab_password": "qwerty1234",
"lights": {
"sign": {
"names": [
"tt-sign",
"1"
],
"settings": {
"colors": [
"#968a00"
],
"brightness": 39,
"pattern": "circle",
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10,
"patterns": {
"pulse": {
"colors": [
"#ff00ff"
],
"delay": 657,
"n1": 100,
"n2": 10,
"n3": 100,
"n4": 10,
"n5": 10,
"n6": 10
},
"n_chase": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10
},
"on": {
"colors": [
"#ff00ff"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10
},
"rainbow": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10
},
"off": {
"colors": [
"#0000ff",
"#ff0000"
],
"delay": 10000,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10
},
"blink": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10
},
"transition": {
"colors": [
"#ff00ff",
"#ffff00"
],
"delay": 10000,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"circle": {
"colors": [
"#0000ff",
"#ff0000"
],
"delay": 10000,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"chase": {
"colors": [
"#000091",
"#00d800"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
}
}
}
},
"dj": {
"names": [
"dj"
],
"settings": {
"colors": [
"#0000ff",
"#ff0000"
],
"brightness": 39,
"pattern": "transition",
"delay": 10000,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10,
"patterns": {
"rainbow": {
"colors": [
"#00006a"
],
"delay": 17,
"n1": 1,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10
},
"on": {
"colors": [
"#ff0062",
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10
},
"blink": {
"colors": [
"#0000d0"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10
},
"pulse": {
"delay": 1002,
"colors": [
"#006600",
"#0000ff"
],
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10
},
"transition": {
"colors": [
"#0000ff",
"#ff0000"
],
"delay": 10000,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10
},
"n_chase": {
"n1": 11,
"n2": 13,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10,
"delay": 639,
"colors": [
"#0000ff"
]
},
"off": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"circle": {
"colors": [
"#0001bd",
"#00ff00"
],
"delay": 1778,
"n1": 20,
"n2": 40,
"n3": 40,
"n4": 0
},
"chase": {
"colors": [
"#8d00ff",
"#ff0077"
],
"delay": 69,
"n1": 30,
"n2": 30,
"n3": 5,
"n4": 30
}
}
}
},
"middle": {
"names": [
"middle1",
"middle2",
"middle3",
"middle4"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 44,
"pattern": "on",
"delay": 520,
"patterns": {
"flicker": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"on": {
"colors": [
"#ff00ff"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"off": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
}
}
}
},
"sides": {
"names": [
"left",
"right"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 13,
"pattern": "on",
"delay": 520,
"patterns": {
"on": {
"colors": [
"#ff00ff"
],
"delay": 988,
"n1": 100,
"n2": 10,
"n3": 10,
"n4": 10
},
"off": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"pulse": {
"n1": 100,
"n2": 100,
"n3": 100,
"n4": 10,
"delay": 411,
"colors": [
"#ff00ff"
]
}
}
}
},
"outside": {
"names": [
"outside"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 76,
"pattern": "on",
"delay": 520,
"n1": -17,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10,
"patterns": {
"on": {
"colors": [
"#ff00ff"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"transition": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"off": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
}
}
}
},
"middle1": {
"names": [
"middle1"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 59,
"pattern": "on",
"delay": 520,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10,
"patterns": {
"flicker": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"on": {
"colors": [
"#ff00ff"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"pulse": {
"delay": 1096,
"colors": [
"#0000ff"
],
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"off": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"rainbow": {
"n1": 1,
"n2": 10,
"n3": 10,
"n4": 10,
"delay": 2884,
"colors": [
"#000000"
]
},
"transition": {
"colors": [
"#0000ff",
"#ff0000"
],
"delay": 269,
"n1": 5,
"n2": 10,
"n3": 10,
"n4": 10
}
}
}
},
"middle2": {
"names": [
"middle2"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 141,
"pattern": "on",
"delay": 520,
"patterns": {
"flicker": {
"colors": [
"#000078"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"on": {
"colors": [
"#ff00ff"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"pulse": {
"colors": [
"#0000a0",
"#720000"
],
"delay": 4102,
"n1": 100,
"n2": 10,
"n3": 100,
"n4": 10
},
"off": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
}
}
}
},
"middle3": {
"names": [
"middle3"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 6,
"pattern": "on",
"delay": 520,
"patterns": {
"flicker": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"on": {
"colors": [
"#00c4a5"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"off": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
}
}
}
},
"middle4": {
"names": [
"middle4"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 6,
"pattern": "on",
"delay": 520,
"patterns": {
"flicker": {
"colors": [
"#ff00d6"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"on": {
"colors": [
"#ff00ff"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"off": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
}
}
}
},
"front1": {
"names": [
"front1"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 255,
"pattern": "on",
"delay": 520,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10,
"patterns": {
"on": {
"colors": [
"#ff00ff",
"#0000ff"
],
"delay": 2409,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"off": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"pulse": {
"colors": [
"#000090"
],
"delay": 1051,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"transition": {
"colors": [
"#ff0000",
"#0000ff"
],
"delay": 2564,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
}
}
}
},
"front2": {
"names": [
"front2"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 255,
"pattern": "off",
"delay": 520,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10,
"patterns": {
"on": {
"colors": [
"#ff00ff"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"off": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"rainbow": {
"colors": [
"#00006b"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"transition": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
}
}
}
},
"front3": {
"names": [
"front3"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 29,
"pattern": "on",
"delay": 520,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"n5": 10,
"n6": 10,
"patterns": {
"on": {
"colors": [
"#d200d1"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"off": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"rainbow": {
"colors": [
"#00006b"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
},
"transition": {
"colors": [
"#000000"
],
"delay": 99,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10
}
}
}
}
},
"tab_order": [
"sign",
"dj",
"middle",
"sides",
"outside",
"middle1",
"middle2",
"middle3",
"middle4",
"front1",
"front2",
"front3"
],
"color_palette": [
"#c33232",
"#3237c3"
]
}

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env python3
"""
Startup script for the Flask web application.
"""
import sys
import os
# Add src directory to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from flask_app import app
if __name__ == '__main__':
print("Starting Lighting Controller Web App...")
print("Open http://localhost:5000 in your browser")
app.run(host='0.0.0.0', port=5000, debug=True)

View File

@@ -1,54 +1,79 @@
{
"tab_password": "",
"current_profile": "default",
"patterns": {
"on": {
"min_delay": 10,
"max_delay": 10000
"lights": {
"1": {
"names": [
"10"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00"
],
"brightness": 9,
"pattern": "on",
"delay": 50
}
},
"off": {
"min_delay": 10,
"max_delay": 10000
"2": {
"names": [
"13"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 6,
"pattern": "flicker",
"delay": 520
}
},
"rainbow": {
"Step Rate": "n1",
"min_delay": 10,
"max_delay": 10000
"3": {
"names": [
"11"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 6,
"pattern": "flicker",
"delay": 520
}
},
"transition": {
"min_delay": 10,
"max_delay": 10000
},
"chase": {
"Colour 1 Length": "n1",
"Colour 2 Length": "n2",
"Step 1": "n3",
"Step 2": "n4",
"min_delay": 10,
"max_delay": 10000
},
"pulse": {
"Attack": "n1",
"Hold": "n2",
"Decay": "n3",
"min_delay": 10,
"max_delay": 10000
},
"circle": {
"Head Rate": "n1",
"Max Length": "n2",
"Tail Rate": "n3",
"Min Length": "n4",
"min_delay": 10,
"max_delay": 10000
},
"blink": {
"min_delay": 10,
"max_delay": 10000
"all": {
"names": [
"10",
"11",
"13"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 6,
"pattern": "on",
"delay": 520
}
}
},
"color_palette": [
"#c12525",
"#246dcc"
"patterns": [
"on",
"off",
"blink",
"rainbow_cycle",
"color_transition",
"theater_chase",
"flicker",
"pulse"
]
}

View File

@@ -1,635 +0,0 @@
"""
Flask web application for the lighting controller.
Provides REST API and serves the web UI.
"""
import asyncio
import json
import os
import math
from flask import Flask, render_template, request, jsonify
from flask_cors import CORS
from networking import WebSocketClient
import color_utils
from settings import Settings
app = Flask(__name__,
template_folder='../templates',
static_folder='../static')
CORS(app)
# Global settings and WebSocket client
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."""
if delay_ms <= min_delay:
return 0
if delay_ms >= max_delay:
return 1000
if min_delay == max_delay:
return 0
return 1000 * math.log(delay_ms / min_delay) / math.log(max_delay / min_delay)
def slider_to_delay(slider_value, min_delay=10, max_delay=10000):
"""Convert slider position (0-1000) to delay in ms using logarithmic scale."""
if slider_value <= 0:
return min_delay
if slider_value >= 1000:
return max_delay
if min_delay == max_delay:
return min_delay
return int(min_delay * ((max_delay / min_delay) ** (slider_value / 1000)))
def get_pattern_settings(tab_name, pattern_name):
"""Get pattern-specific settings."""
light_settings = settings["lights"][tab_name]["settings"]
if "patterns" not in light_settings:
light_settings["patterns"] = {}
if pattern_name not in light_settings["patterns"]:
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", 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)),
}
def save_pattern_settings(tab_name, pattern_name, colors=None, delay=None, n_params=None):
"""Save pattern-specific settings."""
light_settings = settings["lights"][tab_name]["settings"]
if "patterns" not in light_settings:
light_settings["patterns"] = {}
if pattern_name not in light_settings["patterns"]:
light_settings["patterns"][pattern_name] = {}
pattern_settings = light_settings["patterns"][pattern_name]
if colors is not None:
pattern_settings["colors"] = colors
if delay is not None:
pattern_settings["delay"] = delay
if n_params is not None:
for i in range(1, 5):
if f"n{i}" in n_params:
pattern_settings[f"n{i}"] = n_params[f"n{i}"]
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:
current_profile = "default"
settings["current_profile"] = current_profile
# Save current_profile to settings.json
settings.save()
try:
profiles_dir = "profiles"
os.makedirs(profiles_dir, exist_ok=True)
profile_path = os.path.join(profiles_dir, f"{current_profile}.json")
# Get current tab order
tab_order = settings.get("tab_order", [])
if "lights" in settings:
# Ensure all current tabs are in the order
current_tabs = set(settings["lights"].keys())
order_tabs = set(tab_order)
for tab in current_tabs:
if tab not in order_tabs:
tab_order.append(tab)
settings["tab_order"] = tab_order
# Save to profile file (exclude current_profile from profile)
profile_data = dict(settings)
profile_data.pop("current_profile", None)
profile_data.pop("patterns", None) # Patterns stay in settings.json
with open(profile_path, 'w') as file:
json.dump(profile_data, file, indent=4)
print(f"Profile '{current_profile}' saved successfully.")
except Exception as e:
print(f"Error saving profile: {e}")
async def send_to_lighting_controller(payload):
"""Send data to the lighting controller via WebSocket."""
global websocket_client
if websocket_client is None:
websocket_client = WebSocketClient(websocket_uri)
await websocket_client.connect()
if not websocket_client.is_connected:
await websocket_client.connect()
if websocket_client.is_connected:
await websocket_client.send_data(payload)
def run_async(coro):
"""Run async function in sync context."""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
return loop.run_until_complete(coro)
finally:
loop.close()
@app.route('/')
def index():
"""Serve the main web UI."""
return render_template('index.html')
@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", []),
"current_profile": settings.get("current_profile", ""),
"color_palette": settings.get("color_palette", [])
})
@app.route('/api/pattern', methods=['POST'])
def set_pattern():
"""Set the pattern for a light group."""
data = request.json
tab_name = data.get("tab_name")
pattern_name = data.get("pattern")
if not tab_name or not pattern_name:
return jsonify({"error": "Missing tab_name or pattern"}), 400
if tab_name not in settings.get("lights", {}):
return jsonify({"error": f"Tab '{tab_name}' not found"}), 404
# Save current pattern's settings before switching
old_pattern = settings["lights"][tab_name]["settings"].get("pattern", "on")
# Get current delay (would need to be passed from frontend)
current_delay = data.get("delay", 100)
current_n_params = {
f"n{i}": data.get(f"n{i}", 10) for i in range(1, 5)
}
save_pattern_settings(
tab_name,
old_pattern,
colors=data.get("colors", ["#000000"]),
delay=current_delay,
n_params=current_n_params
)
# Load new pattern's settings
new_pattern_settings = get_pattern_settings(tab_name, pattern_name)
# Update settings
settings["lights"][tab_name]["settings"]["pattern"] = pattern_name
# Prepare payload for lighting controller
names = settings["lights"][tab_name]["names"]
payload = {
"save": True,
"names": names,
"settings": {
"pattern": pattern_name,
"brightness": settings["lights"][tab_name]["settings"].get("brightness", 127),
"delay": new_pattern_settings["delay"],
"colors": new_pattern_settings["colors"],
**{f"n{i}": new_pattern_settings[f"n{i}"] for i in range(1, 5)}
}
}
# Send to lighting controller
run_async(send_to_lighting_controller(payload))
settings.save()
return jsonify({
"success": True,
"pattern": pattern_name,
"settings": new_pattern_settings
})
@app.route('/api/parameters', methods=['POST'])
def set_parameters():
"""Update parameters (RGB, brightness, delay, n params) for a light group."""
data = request.json
tab_name = data.get("tab_name")
if not tab_name:
return jsonify({"error": "Missing tab_name"}), 400
if tab_name not in settings.get("lights", {}):
return jsonify({"error": f"Tab '{tab_name}' not found"}), 404
current_pattern = settings["lights"][tab_name]["settings"].get("pattern", "on")
pattern_config = settings.get("patterns", {}).get(current_pattern, {})
min_delay = pattern_config.get("min_delay", 10)
max_delay = pattern_config.get("max_delay", 10000)
# Build settings payload
payload_settings = {}
# Handle RGB colors
if "red" in data or "green" in data or "blue" in data:
r = data.get("red", 0)
g = data.get("green", 0)
b = data.get("blue", 0)
hex_color = f"#{r:02x}{g:02x}{b:02x}"
# Update color in palette
pattern_settings = get_pattern_settings(tab_name, current_pattern)
colors = pattern_settings["colors"].copy()
selected_index = data.get("color_index", 0)
if 0 <= selected_index < len(colors):
colors[selected_index] = hex_color
else:
# If index is out of range, append the color
colors.append(hex_color)
# Save pattern settings to persist the color change
save_pattern_settings(tab_name, current_pattern, colors=colors)
payload_settings["colors"] = colors
# Handle brightness
if "brightness" in data:
brightness = int(data["brightness"])
settings["lights"][tab_name]["settings"]["brightness"] = brightness
payload_settings["brightness"] = brightness
# Handle delay
if "delay_slider" in data:
slider_value = int(data["delay_slider"])
delay = slider_to_delay(slider_value, min_delay, max_delay)
save_pattern_settings(tab_name, current_pattern, delay=delay)
payload_settings["delay"] = delay
# Handle n parameters
n_params = {}
for i in range(1, 5):
if f"n{i}" in data:
n_params[f"n{i}"] = int(data[f"n{i}"])
if n_params:
save_pattern_settings(tab_name, current_pattern, n_params=n_params)
payload_settings.update(n_params)
# Send to lighting controller
if payload_settings:
names = settings["lights"][tab_name]["names"]
payload = {
"save": True,
"names": names,
"settings": payload_settings
}
run_async(send_to_lighting_controller(payload))
# Save to settings.json (for patterns) and to profile file (for lights data)
settings.save()
save_current_profile()
return jsonify({"success": True})
@app.route('/api/tabs', methods=['GET'])
def get_tabs():
"""Get list of tabs."""
return jsonify({
"tabs": settings.get("tab_order", []),
"lights": settings.get("lights", {})
})
@app.route('/api/tabs', methods=['POST'])
def create_tab():
"""Create a new tab."""
data = request.json
tab_name = data.get("name")
ids = data.get("ids", ["1"])
if not tab_name:
return jsonify({"error": "Missing name"}), 400
if tab_name in settings.get("lights", {}):
return jsonify({"error": f"Tab '{tab_name}' already exists"}), 400
settings.setdefault("lights", {})[tab_name] = {
"names": ids if isinstance(ids, list) else [ids],
"settings": {
"pattern": "on",
"brightness": 127,
"colors": ["#000000"],
"delay": 100,
"n1": 10,
"n2": 10,
"n3": 10,
"n4": 10,
"patterns": {}
}
}
if "tab_order" not in settings:
settings["tab_order"] = []
settings["tab_order"].append(tab_name)
settings.save()
save_current_profile()
return jsonify({"success": True, "tab_name": tab_name})
@app.route('/api/tabs/<tab_name>', methods=['PUT'])
def update_tab(tab_name):
"""Update a tab."""
data = request.json
new_name = data.get("name", tab_name)
ids = data.get("ids")
if tab_name not in settings.get("lights", {}):
return jsonify({"error": f"Tab '{tab_name}' not found"}), 404
if new_name != tab_name:
if new_name in settings.get("lights", {}):
return jsonify({"error": f"Tab '{new_name}' already exists"}), 400
# Rename tab
settings["lights"][new_name] = settings["lights"][tab_name]
del settings["lights"][tab_name]
# Update tab order
if "tab_order" in settings and tab_name in settings["tab_order"]:
index = settings["tab_order"].index(tab_name)
settings["tab_order"][index] = new_name
tab_name = new_name
if ids is not None:
settings["lights"][tab_name]["names"] = ids if isinstance(ids, list) else [ids]
settings.save()
save_current_profile()
return jsonify({"success": True, "tab_name": tab_name})
@app.route('/api/tabs/<tab_name>', methods=['DELETE'])
def delete_tab(tab_name):
"""Delete a tab."""
if tab_name not in settings.get("lights", {}):
return jsonify({"error": f"Tab '{tab_name}' not found"}), 404
del settings["lights"][tab_name]
if "tab_order" in settings and tab_name in settings["tab_order"]:
settings["tab_order"].remove(tab_name)
settings.save()
save_current_profile()
return jsonify({"success": True})
@app.route('/api/profiles', methods=['GET'])
def get_profiles():
"""Get list of profiles."""
profiles_dir = "profiles"
profiles = []
if os.path.exists(profiles_dir):
for filename in os.listdir(profiles_dir):
if filename.endswith('.json'):
profiles.append(filename[:-5])
profiles.sort()
return jsonify({
"profiles": profiles,
"current_profile": settings.get("current_profile", ""),
"color_palette": settings.get("color_palette", [])
})
@app.route('/api/profiles', methods=['POST'])
def create_profile():
"""Create a new profile."""
data = request.json
profile_name = data.get("name")
if not profile_name:
return jsonify({"error": "Missing name"}), 400
profiles_dir = "profiles"
os.makedirs(profiles_dir, exist_ok=True)
profile_path = os.path.join(profiles_dir, f"{profile_name}.json")
if os.path.exists(profile_path):
return jsonify({"error": f"Profile '{profile_name}' already exists"}), 400
empty_profile = {
"lights": {},
"tab_password": "",
"tab_order": [],
"color_palette": []
}
with open(profile_path, 'w') as file:
json.dump(empty_profile, file, indent=4)
return jsonify({"success": True, "profile_name": profile_name})
@app.route('/api/profiles/<profile_name>', methods=['DELETE'])
def delete_profile(profile_name):
"""Delete a profile."""
profiles_dir = "profiles"
profile_path = os.path.join(profiles_dir, f"{profile_name}.json")
if not os.path.exists(profile_path):
return jsonify({"error": f"Profile '{profile_name}' not found"}), 404
# Prevent deleting the only existing profile to avoid leaving the app with no profiles
existing_profiles = [
f[:-5] for f in os.listdir(profiles_dir) if f.endswith('.json')
] if os.path.exists(profiles_dir) else []
if len(existing_profiles) <= 1:
return jsonify({"error": "Cannot delete the only existing profile"}), 400
# If deleting the current profile, clear current_profile and related state
if settings.get("current_profile") == profile_name:
settings["current_profile"] = ""
settings["lights"] = {}
settings["tab_order"] = []
settings["color_palette"] = []
# Persist to settings.json
settings_to_save = {
"tab_password": settings.get("tab_password", ""),
"current_profile": "",
"patterns": settings.get("patterns", {})
}
with open("settings.json", 'w') as f:
json.dump(settings_to_save, f, indent=4)
# Remove the profile file
os.remove(profile_path)
return jsonify({"success": True})
@app.route('/api/profiles/<profile_name>', methods=['POST'])
def load_profile(profile_name):
"""Load 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)
# 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"] = 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,
"current_profile": profile_name,
"patterns": patterns_backup
}
with open("settings.json", 'w') as f:
json.dump(settings_to_save, f, indent=4)
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():
"""Initialize WebSocket connection in background."""
global websocket_client
if websocket_client is None:
websocket_client = WebSocketClient(websocket_uri)
run_async(websocket_client.connect())
if __name__ == '__main__':
# Initialize WebSocket connection
init_websocket()
app.run(host='0.0.0.0', port=5000, debug=True)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -9,13 +9,7 @@ class Settings(dict):
def save(self):
try:
# Create a copy without lights and tab_order (these belong in profiles, not settings.json)
# But keep patterns, tab_password, and current_profile
settings_to_save = {k: v for k, v in self.items() if k not in ["lights", "tab_order"]}
# Ensure patterns are always included if they exist
if "patterns" in self:
settings_to_save["patterns"] = self["patterns"]
j = json.dumps(settings_to_save, indent=4)
j = json.dumps(self, indent=4)
with open(self.SETTINGS_FILE, 'w') as file:
file.write(j)
print("Settings saved successfully.")
@@ -27,40 +21,6 @@ class Settings(dict):
with open(self.SETTINGS_FILE, 'r') as file:
loaded_settings = json.load(file)
self.update(loaded_settings)
# Ensure patterns exist (they should always be in settings.json)
if "patterns" not in self:
# Initialize with default patterns if missing
self["patterns"] = {
"on": {"min_delay": 10, "max_delay": 10000},
"off": {"min_delay": 10, "max_delay": 10000},
"rainbow": {"Step Rate": "n1", "min_delay": 10, "max_delay": 10000},
"transition": {"min_delay": 10, "max_delay": 10000},
"chase": {
"Colour 1 Length": "n1",
"Colour 2 Length": "n2",
"Step 1": "n3",
"Step 2": "n4",
"min_delay": 10,
"max_delay": 10000
},
"pulse": {
"Attack": "n1",
"Hold": "n2",
"Decay": "n3",
"min_delay": 10,
"max_delay": 10000
},
"circle": {
"Head Rate": "n1",
"Max Length": "n2",
"Tail Rate": "n3",
"Min Length": "n4",
"min_delay": 10,
"max_delay": 10000
},
"blink": {"min_delay": 10, "max_delay": 10000}
}
self.save() # Save to persist the default patterns
print("Settings loaded successfully.")
except Exception as e:
print(f"Error loading settings {e}")

File diff suppressed because it is too large Load Diff

View File

@@ -1,505 +0,0 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: #2e2e2e;
color: white;
height: 100vh;
overflow: hidden;
}
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
}
header {
background-color: #1a1a1a;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #4a4a4a;
}
header h1 {
font-size: 1.5rem;
font-weight: 600;
}
.header-actions {
display: flex;
gap: 0.5rem;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: background-color 0.2s;
}
.btn-primary {
background-color: #4a4a4a;
color: white;
}
.btn-primary:hover {
background-color: #5a5a5a;
}
.btn-secondary {
background-color: #3a3a3a;
color: white;
}
.btn-secondary:hover {
background-color: #4a4a4a;
}
.btn-danger {
background-color: #d32f2f;
color: white;
}
.btn-danger:hover {
background-color: #c62828;
}
.btn-small {
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tabs-container {
background-color: #1a1a1a;
border-bottom: 2px solid #4a4a4a;
padding: 0.5rem 1rem;
}
.tabs-list {
display: flex;
gap: 0.5rem;
overflow-x: auto;
}
.tab-button {
padding: 0.5rem 1rem;
background-color: #3a3a3a;
color: white;
border: none;
border-radius: 4px 4px 0 0;
cursor: pointer;
font-size: 0.9rem;
white-space: nowrap;
transition: background-color 0.2s;
}
.tab-button:hover {
background-color: #4a4a4a;
}
.tab-button.active {
background-color: #6a5acd;
color: white;
}
.tab-content {
flex: 1;
display: flex;
overflow: hidden;
padding: 1rem;
gap: 1rem;
}
.left-panel {
flex: 0 0 50%;
display: flex;
flex-direction: column;
gap: 1rem;
overflow-y: auto;
border-right: 2px solid #4a4a4a;
padding-right: 1rem;
}
.right-panel {
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
overflow-y: auto;
padding-left: 1rem;
}
.ids-display {
padding: 0.5rem;
background-color: #3a3a3a;
border-radius: 4px;
font-size: 0.9rem;
}
.controls-section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.control-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.control-group label {
min-width: 100px;
font-weight: 500;
}
.slider {
flex: 1;
height: 8px;
background-color: #3a3a3a;
border-radius: 4px;
outline: none;
-webkit-appearance: none;
margin: 0 0.5rem;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
background-color: #6a5acd;
border-radius: 50%;
cursor: pointer;
transition: background-color 0.2s;
}
.slider::-webkit-slider-thumb:hover {
background-color: #7a6add;
}
.slider::-moz-range-thumb {
width: 20px;
height: 20px;
background-color: #6a5acd;
border-radius: 50%;
cursor: pointer;
border: none;
transition: background-color 0.2s;
}
.slider::-moz-range-thumb:hover {
background-color: #7a6add;
}
/* Red slider */
#red-slider {
accent-color: #ff0000;
}
#red-slider::-webkit-slider-thumb {
background-color: #ff0000;
}
#red-slider::-moz-range-thumb {
background-color: #ff0000;
}
/* Green slider */
#green-slider {
accent-color: #00ff00;
}
#green-slider::-webkit-slider-thumb {
background-color: #00ff00;
}
#green-slider::-moz-range-thumb {
background-color: #00ff00;
}
/* Blue slider */
#blue-slider {
accent-color: #0000ff;
}
#blue-slider::-webkit-slider-thumb {
background-color: #0000ff;
}
#blue-slider::-moz-range-thumb {
background-color: #0000ff;
}
/* Brightness slider */
#brightness-slider {
accent-color: #ffff00;
}
#brightness-slider::-webkit-slider-thumb {
background-color: #ffff00;
}
#brightness-slider::-moz-range-thumb {
background-color: #ffff00;
}
.slider-value {
min-width: 50px;
text-align: right;
font-weight: 500;
font-size: 0.9rem;
}
.n-params-section {
margin-top: 1rem;
}
.n-params-section h3 {
margin-bottom: 0.5rem;
font-size: 1rem;
}
.n-params-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.n-param-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.n-param-group label {
min-width: 40px;
font-weight: 500;
}
.n-input {
flex: 1;
padding: 0.5rem;
background-color: #3a3a3a;
color: white;
border: 1px solid #4a4a4a;
border-radius: 4px;
font-size: 1rem;
}
.n-input:focus {
outline: none;
border-color: #6a5acd;
}
.patterns-section,
.color-palette-section {
background-color: #1a1a1a;
border: 2px solid #4a4a4a;
border-radius: 4px;
padding: 1rem;
}
.patterns-section h3,
.color-palette-section h3 {
margin-bottom: 1rem;
font-size: 1.1rem;
}
.patterns-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.pattern-button {
padding: 0.75rem;
background-color: #3a3a3a;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
text-align: left;
transition: background-color 0.2s;
}
.pattern-button:hover {
background-color: #4a4a4a;
}
.pattern-button.active {
background-color: #6a5acd;
color: white;
}
.color-palette {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
max-height: 300px;
overflow-y: auto;
}
.color-swatch {
display: flex;
align-items: center;
padding: 0.5rem;
background-color: #3a3a3a;
border: 2px solid transparent;
border-radius: 4px;
cursor: pointer;
transition: border-color 0.2s;
gap: 0.5rem;
}
.color-swatch:hover {
border-color: #6a5acd;
}
.color-swatch.selected {
border-color: #FFD700;
border-width: 3px;
}
.color-swatch-preview {
width: 40px;
height: 40px;
border-radius: 4px;
border: 1px solid #4a4a4a;
flex-shrink: 0;
}
.color-swatch-label {
flex: 1;
font-size: 0.9rem;
min-width: 80px;
}
.color-picker-input {
width: 60px;
height: 40px;
border: 1px solid #4a4a4a;
border-radius: 4px;
cursor: pointer;
background: none;
padding: 0;
flex-shrink: 0;
}
.color-picker-input::-webkit-color-swatch-wrapper {
padding: 0;
}
.color-picker-input::-webkit-color-swatch {
border: none;
border-radius: 4px;
}
.color-picker-input::-moz-color-swatch {
border: none;
border-radius: 4px;
}
.palette-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background-color: #2e2e2e;
padding: 2rem;
border-radius: 8px;
min-width: 400px;
max-width: 500px;
}
.modal-content h2 {
margin-bottom: 1rem;
font-size: 1.3rem;
}
.modal-content label {
display: block;
margin-top: 1rem;
margin-bottom: 0.5rem;
font-weight: 500;
}
.modal-content input {
width: 100%;
padding: 0.5rem;
background-color: #3a3a3a;
color: white;
border: 1px solid #4a4a4a;
border-radius: 4px;
font-size: 1rem;
}
.modal-content input:focus {
outline: none;
border-color: #6a5acd;
}
.modal-actions {
display: flex;
gap: 0.5rem;
margin-top: 1.5rem;
justify-content: flex-end;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1a1a1a;
}
::-webkit-scrollbar-thumb {
background: #4a4a4a;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #5a5a5a;
}

View File

@@ -1,194 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lighting Controller</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="app-container">
<header>
<h1>Lighting Controller</h1>
<div class="header-actions">
<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="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>
</div>
</header>
<div class="main-content">
<div class="tabs-container">
<div id="tabs-list" class="tabs-list"></div>
</div>
<div id="tab-content" class="tab-content">
<div class="left-panel">
<div class="ids-display">
<label>IDs: </label>
<span id="current-ids"></span>
</div>
<div class="color-palette-section">
<h3>Color Palette</h3>
<div id="color-palette" class="color-palette"></div>
<div class="palette-actions">
<button id="add-color-btn" class="btn btn-small">Add Color</button>
<button id="remove-color-btn" class="btn btn-small">Remove Selected</button>
</div>
</div>
<div class="controls-section">
<div class="control-group">
<label for="brightness-slider">Brightness:</label>
<input type="range" id="brightness-slider" min="0" max="255" value="127" class="slider">
<span id="brightness-value" class="slider-value">127</span>
</div>
<div class="control-group">
<label for="delay-slider">Delay:</label>
<input type="range" id="delay-slider" min="0" max="1000" value="0" class="slider">
<span id="delay-value" class="slider-value">100 ms</span>
</div>
</div>
<div class="n-params-section">
<h3>N Parameters</h3>
<div class="n-params-grid">
<div class="n-param-group">
<label for="n1-input">n1:</label>
<input type="number" id="n1-input" min="0" max="255" value="10" class="n-input">
</div>
<div class="n-param-group">
<label for="n2-input">n2:</label>
<input type="number" id="n2-input" min="0" max="255" value="10" class="n-input">
</div>
<div class="n-param-group">
<label for="n3-input">n3:</label>
<input type="number" id="n3-input" min="0" max="255" value="10" class="n-input">
</div>
<div class="n-param-group">
<label for="n4-input">n4:</label>
<input type="number" id="n4-input" min="0" max="255" value="10" class="n-input">
</div>
</div>
</div>
</div>
<div class="right-panel">
<div class="patterns-section">
<h3>Patterns</h3>
<div id="patterns-list" class="patterns-list"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Modals -->
<div id="add-tab-modal" class="modal">
<div class="modal-content">
<h2>Add New Tab</h2>
<label>Tab Name:</label>
<input type="text" id="new-tab-name" placeholder="Enter tab name">
<label>Device IDs (comma-separated):</label>
<input type="text" id="new-tab-ids" placeholder="1,2,3" value="1">
<div class="modal-actions">
<button id="add-tab-confirm" class="btn btn-primary">Add</button>
<button id="add-tab-cancel" class="btn btn-secondary">Cancel</button>
</div>
</div>
</div>
<div id="edit-tab-modal" class="modal">
<div class="modal-content">
<h2>Edit Tab</h2>
<label>Tab Name:</label>
<input type="text" id="edit-tab-name" placeholder="Enter tab name">
<label>Device IDs (comma-separated):</label>
<input type="text" id="edit-tab-ids" placeholder="1,2,3">
<div class="modal-actions">
<button id="edit-tab-confirm" class="btn btn-primary">Update</button>
<button id="edit-tab-cancel" class="btn btn-secondary">Cancel</button>
</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>
</body>
</html>

View File

@@ -1,2 +0,0 @@
This is just a placeholder to satisfy the tool requirement; actual code changes are in other files.