17 Commits

Author SHA1 Message Date
ce3b9f4ea5 Add profile deletion feature
- Added DELETE endpoint /api/profiles/<profile_name> to delete profiles
- Prevent deletion of the only remaining profile
- Clear current profile state if the active profile is deleted
- Added Delete button next to each profile in the Profiles modal
- Added confirmation dialog before deleting profiles
- Automatically refresh profile list after deletion
2026-01-05 23:09:10 +13:00
40cfe19759 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
2026-01-05 22:42:58 +13:00
c97ca308a7 Add profile persistence for color changes and data saving
- Added save_current_profile() function to persist lights data to profile files
- Updated all endpoints to save to profile files after changes
- Ensures color changes, pattern changes, and tab modifications are persisted
- Data now saves to both settings.json (patterns) and profile files (lights data)
2026-01-04 16:07:54 +13:00
5aa500a7fb Convert app to Flask web application with color pickers
- Created Flask backend with REST API endpoints
- Built HTML/CSS/JavaScript frontend
- Replaced RGB sliders with color pickers for each palette color
- Reorganized layout: color palette on left, patterns on right
- Added persistence for color changes
- Integrated WebSocket client for lighting controller communication
- Added tab management, profile support, and pattern selection
2026-01-04 15:59:19 +13:00
c8ae113355 Remove associated names label and always show n parameter inputs 2025-11-30 17:23:32 +13:00
2db2d9e120 Fix bottom menu buttons visibility by adjusting packing order 2025-11-30 17:07:21 +13:00
42575b9d2e Fix profile loading to not modify settings.json, preserve patterns 2025-11-30 17:03:43 +13:00
517750e5f6 Add patterns configuration to settings.json 2025-11-30 16:52:09 +13:00
5e4798a9dc Remove scrolling and fix empty space, restore patterns to settings.json 2025-11-30 16:49:54 +13:00
fb4944e475 Add screen resolution scaling and move tab buttons to bottom 2025-11-30 16:44:14 +13:00
c5a76c24a7 Move patterns to settings.json and remove patterns.json 2025-11-30 16:31:22 +13:00
ce8596ca58 Add tab management, profiles, and pattern-specific delay ranges 2025-11-30 16:23:08 +13:00
92526ab05c Move patterns to separate patterns.json file 2025-11-30 14:43:22 +13:00
8dabf852ba Show descriptive names for n parameters based on selected pattern 2025-11-30 13:27:35 +13:00
e803dd4243 Update main 2025-11-21 16:18:08 +13:00
baf3d0b0ff Update settings 2025-11-21 16:17:42 +13:00
7f43b93cb7 Update UI: shorter sliders, add n1-n6 inputs, logarithmic delay scale 2025-11-19 23:05:51 +13:00
16 changed files with 8164 additions and 257 deletions

View File

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

72
profiles/default.json Normal file
View File

@@ -0,0 +1,72 @@
{
"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"
]
}

26
profiles/ring.json Normal file
View File

@@ -0,0 +1,26 @@
{
"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"
]
}

6
profiles/test.json Normal file
View File

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

868
profiles/tt.json Normal file
View File

@@ -0,0 +1,868 @@
{
"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"
]
}

17
run_web.py Executable file
View File

@@ -0,0 +1,17 @@
#!/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,162 +1,54 @@
{
"lights": {
"sign": {
"names": [
"tt-sign",
"1"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00"
],
"brightness": 9,
"pattern": "off",
"delay": 50
}
"tab_password": "",
"current_profile": "default",
"patterns": {
"on": {
"min_delay": 10,
"max_delay": 10000
},
"dj": {
"names": [
"dj"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 6,
"pattern": "flicker",
"delay": 520
}
"off": {
"min_delay": 10,
"max_delay": 10000
},
"middle": {
"names": [
"middle1",
"middle2",
"middle3",
"middle4"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 6,
"pattern": "flicker",
"delay": 520
}
"rainbow": {
"Step Rate": "n1",
"min_delay": 10,
"max_delay": 10000
},
"sides": {
"names": [
"left",
"right"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 6,
"pattern": "on",
"delay": 520
}
"transition": {
"min_delay": 10,
"max_delay": 10000
},
"outside": {
"names": [
"outside"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 6,
"pattern": "on",
"delay": 520
}
"chase": {
"Colour 1 Length": "n1",
"Colour 2 Length": "n2",
"Step 1": "n3",
"Step 2": "n4",
"min_delay": 10,
"max_delay": 10000
},
"middle1": {
"names": [
"middle1"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 6,
"pattern": "flicker",
"delay": 520
}
"pulse": {
"Attack": "n1",
"Hold": "n2",
"Decay": "n3",
"min_delay": 10,
"max_delay": 10000
},
"middle2": {
"names": [
"middle2"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 6,
"pattern": "flicker",
"delay": 520
}
"circle": {
"Head Rate": "n1",
"Max Length": "n2",
"Tail Rate": "n3",
"Min Length": "n4",
"min_delay": 10,
"max_delay": 10000
},
"middle3": {
"names": [
"middle3"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 6,
"pattern": "flicker",
"delay": 520
}
},
"middle4": {
"names": [
"middle4"
],
"settings": {
"colors": [
"#0000ff",
"#c30074",
"#00ff00",
"#000000"
],
"brightness": 6,
"pattern": "flicker",
"delay": 520
}
"blink": {
"min_delay": 10,
"max_delay": 10000
}
},
"patterns": [
"on",
"off",
"blink",
"rainbow_cycle",
"color_transition",
"theater_chase",
"flicker",
"pulse"
"color_palette": [
"#c12525",
"#246dcc"
]
}

635
src/flask_app.py Normal file
View File

@@ -0,0 +1,635 @@
"""
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

1054
src/main_textual.py Normal file

File diff suppressed because it is too large Load Diff

2135
src/main_tkinter.py.bak Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,13 @@ class Settings(dict):
def save(self):
try:
j = json.dumps(self, indent=4)
# 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)
with open(self.SETTINGS_FILE, 'w') as file:
file.write(j)
print("Settings saved successfully.")
@@ -21,6 +27,40 @@ 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}")

1068
static/app.js Normal file

File diff suppressed because it is too large Load Diff

505
static/style.css Normal file
View File

@@ -0,0 +1,505 @@
* {
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;
}

194
templates/index.html Normal file
View File

@@ -0,0 +1,194 @@
<!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>

2
tmp_explanation.txt Normal file
View File

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