From ce8596ca587b1e1e82bdac08082e812c433424d1 Mon Sep 17 00:00:00 2001 From: jimmy Date: Sun, 30 Nov 2025 16:23:08 +1300 Subject: [PATCH] Add tab management, profiles, and pattern-specific delay ranges --- profiles/ring.json | 67 ++++ profiles/tt.json | 864 ++++++++++++++++++++++++++++++++++++++++ settings.json | 848 +-------------------------------------- src/main.py | 974 +++++++++++++++++++++++++++++++++++++++++++-- src/settings.py | 4 +- 5 files changed, 1879 insertions(+), 878 deletions(-) create mode 100644 profiles/ring.json create mode 100644 profiles/tt.json diff --git a/profiles/ring.json b/profiles/ring.json new file mode 100644 index 0000000..7f088eb --- /dev/null +++ b/profiles/ring.json @@ -0,0 +1,67 @@ +{ + "lights": { + "ring1": { + "names": [ + "dj" + ], + "settings": { + "pattern": "on", + "brightness": 127, + "colors": [ + "#000000" + ], + "delay": 100, + "n1": 10, + "n2": 10, + "n3": 10, + "n4": 10, + "patterns": { + "on": { + "colors": [ + "#000000" + ], + "delay": 99, + "n1": 10, + "n2": 10, + "n3": 10, + "n4": 10 + } + } + } + }, + "ring2": { + "names": [ + "ring2" + ], + "settings": { + "pattern": "on", + "brightness": 127, + "colors": [ + "#000000" + ], + "delay": 100, + "n1": 10, + "n2": 10, + "n3": 10, + "n4": 10, + "patterns": { + "on": { + "colors": [ + "#000000" + ], + "delay": 99, + "n1": 10, + "n2": 10, + "n3": 10, + "n4": 10 + } + } + } + } + }, + "tab_password": "", + "tab_order": [ + "ring1", + "ring2" + ] +} \ No newline at end of file diff --git a/profiles/tt.json b/profiles/tt.json new file mode 100644 index 0000000..7628840 --- /dev/null +++ b/profiles/tt.json @@ -0,0 +1,864 @@ +{ + "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_password": "qwerty1234", + "tab_order": [ + "sign", + "dj", + "middle", + "sides", + "outside", + "middle1", + "middle2", + "middle3", + "middle4", + "front1", + "front2", + "front3" + ] +} \ No newline at end of file diff --git a/settings.json b/settings.json index 0d41516..1e10474 100644 --- a/settings.json +++ b/settings.json @@ -1,848 +1,4 @@ { - "lights": { - "sign": { - "names": [ - "tt-sign", - "1" - ], - "settings": { - "colors": [ - "#968a00" - ], - "brightness": 39, - "pattern": "rainbow", - "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": [ - "#0000f8" - ], - "delay": 1538, - "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": "circle", - "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_password": "qwerty1234", + "current_profile": "tt" } \ No newline at end of file diff --git a/src/main.py b/src/main.py index 3f0a393..5837595 100644 --- a/src/main.py +++ b/src/main.py @@ -2,6 +2,7 @@ import asyncio import tkinter as tk from tkinter import ttk, messagebox # Import messagebox for confirmations import json +import os from async_tkinter_loop import async_handler, async_mainloop from networking import WebSocketClient import color_utils @@ -23,24 +24,28 @@ highlight_pattern_color = "#6a5acd" active_palette_color_border = "#FFD700" # Gold color -def delay_to_slider(delay_ms): - """Convert delay in ms (10-10000) to slider position (0-1000) using logarithmic scale.""" - if delay_ms <= 10: +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 >= 10000: + if delay_ms >= max_delay: return 1000 - # Logarithmic conversion: delay = 10 * (10000/10) ^ (position/1000) - # Solving for position: position = 1000 * log(delay/10) / log(1000) - return 1000 * math.log(delay_ms / 10) / math.log(1000) + # Logarithmic conversion: delay = min * (max/min) ^ (position/1000) + # Solving for position: position = 1000 * log(delay/min) / log(max/min) + 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): - """Convert slider position (0-1000) to delay in ms (10-10000) using logarithmic scale.""" +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 10 + return min_delay if slider_value >= 1000: - return 10000 - # Logarithmic conversion: delay = 10 * 1000 ^ (position/1000) - return int(10 * (1000 ** (slider_value / 1000))) + return max_delay + # Logarithmic conversion: delay = min * (max/min) ^ (position/1000) + if min_delay == max_delay: + return min_delay + return int(min_delay * ((max_delay / min_delay) ** (slider_value / 1000))) class App: @@ -87,6 +92,142 @@ class App: self.notebook = ttk.Notebook(self.root) self.notebook.pack(expand=1, fill="both") + # Tab management buttons frame + tab_management_frame = tk.Frame(self.root, bg=bg_color) + tab_management_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=5) + + add_tab_btn = tk.Button( + tab_management_frame, + text="+ Add Tab", + command=self.add_tab_dialog, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 14), + padx=10, + pady=5 + ) + add_tab_btn.pack(side=tk.LEFT, padx=5) + + edit_tab_btn = tk.Button( + tab_management_frame, + text="✎ Edit Tab", + command=self.edit_tab_dialog, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 14), + padx=10, + pady=5 + ) + edit_tab_btn.pack(side=tk.LEFT, padx=5) + + delete_tab_btn = tk.Button( + tab_management_frame, + text="✗ Delete Tab", + command=self.delete_tab_dialog, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 14), + padx=10, + pady=5 + ) + delete_tab_btn.pack(side=tk.LEFT, padx=5) + + # Tab reorder buttons + move_left_btn = tk.Button( + tab_management_frame, + text="← Move Left", + command=self.move_tab_left, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 14), + padx=10, + pady=5 + ) + move_left_btn.pack(side=tk.LEFT, padx=5) + + move_right_btn = tk.Button( + tab_management_frame, + text="→ Move Right", + command=self.move_tab_right, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 14), + padx=10, + pady=5 + ) + move_right_btn.pack(side=tk.LEFT, padx=5) + + # Profile dropdown + tk.Label(tab_management_frame, text="Profile:", bg=bg_color, fg=fg_color, font=("Arial", 14)).pack(side=tk.LEFT, padx=(20, 5)) + + self.profile_var = tk.StringVar() + self.profile_dropdown = ttk.Combobox( + tab_management_frame, + textvariable=self.profile_var, + font=("Arial", 14), + width=20, + state="readonly" + ) + self.profile_dropdown.pack(side=tk.LEFT, padx=5) + self.profile_dropdown.bind("<>", self.on_profile_selected) + + # New profile button + new_profile_btn = tk.Button( + tab_management_frame, + text="+ New Profile", + command=self.new_profile_dialog, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 14), + padx=10, + pady=5 + ) + new_profile_btn.pack(side=tk.LEFT, padx=5) + + # Save profile button + save_profile_btn = tk.Button( + tab_management_frame, + text="💾 Save Profile", + command=self.save_profile_dialog, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 14), + padx=10, + pady=5 + ) + save_profile_btn.pack(side=tk.LEFT, padx=5) + + # Load profiles + self.profiles_dir = "profiles" + os.makedirs(self.profiles_dir, exist_ok=True) + self.refresh_profiles() + + # Load current profile if specified + current_profile = self.settings.get("current_profile") + if current_profile: + try: + # Load profile without showing dialog + profile_path = os.path.join(self.profiles_dir, f"{current_profile}.json") + if os.path.exists(profile_path): + with open(profile_path, 'r') as file: + profile_data = json.load(file) + + # Update settings with profile data + # Remove current_profile from profile data if it exists (shouldn't be in profiles) + profile_data.pop("current_profile", None) + self.settings.clear() + self.settings.update(profile_data) + self.settings["current_profile"] = current_profile # Store in settings.json, not profile + # Ensure tab_order exists in profile + if "tab_order" not in self.settings: + if "lights" in self.settings: + self.settings["tab_order"] = list(self.settings["lights"].keys()) + else: + self.settings["tab_order"] = [] + self.settings.save() + except Exception as e: + print(f"Error loading current profile '{current_profile}': {e}") + self.tabs = {} self.create_tabs() @@ -96,11 +237,277 @@ class App: async_mainloop(self.root) - def load_patterns(self): - """Load patterns from patterns.json file.""" + def new_profile_dialog(self): + """Open dialog to create a new empty profile""" + dialog = tk.Toplevel(self.root) + dialog.title("New Profile") + dialog.configure(bg=bg_color) + dialog.transient(self.root) + dialog.grab_set() + + # Center the dialog + dialog.geometry("500x200") + dialog.update_idletasks() + x = (dialog.winfo_screenwidth() // 2) - (dialog.winfo_width() // 2) + y = (dialog.winfo_screenheight() // 2) - (dialog.winfo_height() // 2) + dialog.geometry(f"+{x}+{y}") + + tk.Label(dialog, text="Profile Name:", bg=bg_color, fg=fg_color, font=("Arial", 14)).pack(pady=20) + name_entry = tk.Entry(dialog, font=("Arial", 14), width=30) + name_entry.pack(pady=10) + name_entry.focus() + + def create_profile(): + profile_name = name_entry.get().strip() + if not profile_name: + messagebox.showerror("Error", "Profile name cannot be empty", parent=dialog) + return + + # Check if profile exists + profile_path = os.path.join(self.profiles_dir, f"{profile_name}.json") + if os.path.exists(profile_path): + messagebox.showerror("Error", f"Profile '{profile_name}' already exists", parent=dialog) + return + + try: + # Create empty profile with same structure as settings.json + empty_profile = { + "lights": {}, + "tab_password": "", + "tab_order": [] + } + + with open(profile_path, 'w') as file: + json.dump(empty_profile, file, indent=4) + + # Load the new profile + self.load_profile(profile_name) + + # Refresh profiles dropdown + self.refresh_profiles() + + messagebox.showinfo("Success", f"Empty profile '{profile_name}' created and loaded", parent=dialog) + dialog.destroy() + except Exception as e: + messagebox.showerror("Error", f"Failed to create profile: {e}", parent=dialog) + + button_frame = tk.Frame(dialog, bg=bg_color) + button_frame.pack(pady=20) + + tk.Button( + button_frame, + text="Create", + command=create_profile, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 18), + padx=40, + pady=15, + width=10 + ).pack(side=tk.LEFT, padx=5) + + tk.Button( + button_frame, + text="Cancel", + command=dialog.destroy, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 18), + padx=40, + pady=15, + width=10 + ).pack(side=tk.LEFT, padx=5) + + name_entry.bind("", lambda e: create_profile()) + + def refresh_profiles(self): + """Refresh the list of available profiles""" try: - with open("patterns.json", 'r') as file: - patterns = json.load(file) + profiles = [] + if os.path.exists(self.profiles_dir): + for filename in os.listdir(self.profiles_dir): + if filename.endswith('.json'): + profiles.append(filename[:-5]) # Remove .json extension + profiles.sort() + self.profile_dropdown['values'] = profiles + if profiles: + # Try to load current profile name from settings + current_profile = self.settings.get("current_profile", "") + if current_profile in profiles: + self.profile_var.set(current_profile) + else: + self.profile_var.set("") + except Exception as e: + print(f"Error refreshing profiles: {e}") + + def on_profile_selected(self, event=None): + """Handle profile selection from dropdown""" + selected_profile = self.profile_var.get() + if not selected_profile: + return + + # Confirm before loading (will overwrite current settings) + result = messagebox.askyesno( + "Load Profile", + f"Load profile '{selected_profile}'?\n\nThis will replace your current settings.", + icon="question" + ) + + if result: + self.load_profile(selected_profile) + + def load_profile(self, profile_name): + """Load a profile from the profiles directory""" + try: + profile_path = os.path.join(self.profiles_dir, f"{profile_name}.json") + if not os.path.exists(profile_path): + messagebox.showerror("Error", f"Profile '{profile_name}' not found") + return + + with open(profile_path, 'r') as file: + profile_data = json.load(file) + + # Update settings with profile data + # Remove current_profile from profile data if it exists (shouldn't be in profiles) + profile_data.pop("current_profile", None) + self.settings.clear() + self.settings.update(profile_data) + self.settings["current_profile"] = profile_name # Store in settings.json, not profile + # Ensure tab_order exists in profile + if "tab_order" not in self.settings: + if "lights" in self.settings: + self.settings["tab_order"] = list(self.settings["lights"].keys()) + else: + self.settings["tab_order"] = [] + self.settings.save() + + # Recreate tabs with new settings + self.create_tabs() + + # Select first tab if available + if self.settings.get("lights"): + self.notebook.select(0) + + messagebox.showinfo("Success", f"Profile '{profile_name}' loaded successfully") + except Exception as e: + messagebox.showerror("Error", f"Failed to load profile: {e}") + + def save_profile_dialog(self): + """Open dialog to save current settings as a profile""" + dialog = tk.Toplevel(self.root) + dialog.title("Save Profile") + dialog.configure(bg=bg_color) + dialog.transient(self.root) + dialog.grab_set() + + # Center the dialog + dialog.geometry("500x200") + dialog.update_idletasks() + x = (dialog.winfo_screenwidth() // 2) - (dialog.winfo_width() // 2) + y = (dialog.winfo_screenheight() // 2) - (dialog.winfo_height() // 2) + dialog.geometry(f"+{x}+{y}") + + tk.Label(dialog, text="Profile Name:", bg=bg_color, fg=fg_color, font=("Arial", 14)).pack(pady=20) + name_entry = tk.Entry(dialog, font=("Arial", 14), width=30) + name_entry.pack(pady=10) + name_entry.focus() + + # Pre-fill with current profile if exists + current_profile = self.settings.get("current_profile", "") + if current_profile: + name_entry.insert(0, current_profile) + + def save_profile(): + profile_name = name_entry.get().strip() + if not profile_name: + messagebox.showerror("Error", "Profile name cannot be empty", parent=dialog) + return + + # Check if profile exists + profile_path = os.path.join(self.profiles_dir, f"{profile_name}.json") + overwrite = False + if os.path.exists(profile_path): + result = messagebox.askyesno( + "Overwrite?", + f"Profile '{profile_name}' already exists. Overwrite?", + parent=dialog + ) + if not result: + return + overwrite = True + + try: + # Get current tab order from notebook + tab_order = [] + for i in range(self.notebook.index("end")): + tab_text = self.notebook.tab(i, "text") + if "lights" in self.settings and tab_text in self.settings["lights"]: + tab_order.append(tab_text) + + # Save current settings as profile (exclude current_profile from profile file) + profile_data = dict(self.settings) + profile_data.pop("current_profile", None) # Remove current_profile from profile + profile_data["tab_order"] = tab_order + + with open(profile_path, 'w') as file: + json.dump(profile_data, file, indent=4) + + # Update current profile in settings.json (not in profile file) + self.settings["current_profile"] = profile_name + self.settings["tab_order"] = tab_order + self.settings.save() + + # Refresh profiles dropdown + self.refresh_profiles() + + if overwrite: + messagebox.showinfo("Success", f"Profile '{profile_name}' updated successfully", parent=dialog) + else: + messagebox.showinfo("Success", f"Profile '{profile_name}' saved successfully", parent=dialog) + + dialog.destroy() + except Exception as e: + messagebox.showerror("Error", f"Failed to save profile: {e}", parent=dialog) + + button_frame = tk.Frame(dialog, bg=bg_color) + button_frame.pack(pady=20) + + tk.Button( + button_frame, + text="Save", + command=save_profile, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 18), + padx=40, + pady=15, + width=10 + ).pack(side=tk.LEFT, padx=5) + + tk.Button( + button_frame, + text="Cancel", + command=dialog.destroy, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 18), + padx=40, + pady=15, + width=10 + ).pack(side=tk.LEFT, padx=5) + + name_entry.bind("", lambda e: save_profile()) + + def load_patterns(self): + """Load patterns from settings.json file.""" + try: + # Load patterns from settings.json + patterns = self.settings.get("patterns", {}) + if not patterns: + # If patterns don't exist, create default empty dict + patterns = {} + self.settings["patterns"] = patterns + self.settings.save() print("Patterns loaded successfully.") return patterns except Exception as e: @@ -179,16 +586,65 @@ class App: asyncio.create_task(self.websocket_client.close()) self.root.destroy() + def get_tab_order(self): + """Get the tab order from current profile, or create default order from current lights""" + if "tab_order" not in self.settings: + # Initialize tab_order from current lights keys + if "lights" in self.settings: + self.settings["tab_order"] = list(self.settings["lights"].keys()) + else: + self.settings["tab_order"] = [] + # Ensure all current tabs are in the order + if "lights" in self.settings: + current_tabs = set(self.settings["lights"].keys()) + order_tabs = set(self.settings["tab_order"]) + # Add any missing tabs to the end + for tab in current_tabs: + if tab not in order_tabs: + self.settings["tab_order"].append(tab) + # Remove tabs that no longer exist + self.settings["tab_order"] = [tab for tab in self.settings["tab_order"] if tab in current_tabs] + return self.settings.get("tab_order", []) + + def save_profile(self): + """Save current settings to the active profile""" + current_profile = self.settings.get("current_profile") + if not current_profile: + return + + try: + profile_path = os.path.join(self.profiles_dir, f"{current_profile}.json") + # Get current tab order from notebook + tab_order = [] + for i in range(self.notebook.index("end")): + tab_text = self.notebook.tab(i, "text") + if "lights" in self.settings and tab_text in self.settings["lights"]: + tab_order.append(tab_text) + self.settings["tab_order"] = tab_order + + # Save to profile file (exclude current_profile from profile) + profile_data = dict(self.settings) + profile_data.pop("current_profile", None) # Remove current_profile from profile + with open(profile_path, 'w') as file: + json.dump(profile_data, file, indent=4) + except Exception as e: + print(f"Error saving profile: {e}") + def create_tabs(self): for tab_name in list(self.tabs.keys()): self.notebook.forget(self.tabs[tab_name]) del self.tabs[tab_name] - for key, value in self.settings["lights"].items(): - tab = ttk.Frame(self.notebook) - self.notebook.add(tab, text=key) - self.create_light_control_widgets(tab, key, value["names"], value["settings"]) - self.tabs[key] = tab + # Use stored tab order + tab_order = self.get_tab_order() + + for key in tab_order: + if key in self.settings["lights"]: + value = self.settings["lights"][key] + tab = ttk.Frame(self.notebook) + self.notebook.add(tab, text=key) + self.create_light_control_widgets(tab, key, value["names"], value["settings"]) + self.tabs[key] = tab def create_light_control_widgets(self, tab, tab_name, ids, initial_settings): slider_length = 600 @@ -283,19 +739,33 @@ class App: delay_container = tk.Frame(slider_panel_frame, bg=bg_color) delay_container.pack(side=tk.LEFT, padx=10) + # Get pattern-specific min/max delay + pattern_config = self.patterns.get(initial_pattern, {}) + min_delay = pattern_config.get("min_delay", 10) + max_delay = pattern_config.get("max_delay", 10000) + delay_slider = tk.Scale(delay_container, **delay_slider_config) # Convert initial delay to slider position using logarithmic scale - initial_slider_pos = delay_to_slider(initial_delay) + initial_slider_pos = delay_to_slider(initial_delay, min_delay, max_delay) delay_slider.set(initial_slider_pos) # Create a custom label to show the actual delay value, positioned like the default Scale value delay_value_label = tk.Label(delay_container, text=f"{initial_delay}", font=("Arial", 12), bg=bg_color, fg=fg_color, width=5, anchor="e") delay_value_label.pack(side=tk.LEFT, padx=(0, 5)) + # Store min/max delay in tab widget for later use + tab.min_delay = min_delay + tab.max_delay = max_delay + # Function to update the side label when slider moves def update_delay_value_label(event=None): slider_value = delay_slider.get() - actual_delay = slider_to_delay(slider_value) + # Use current pattern's min/max delay (may change if pattern changes) + current_pattern = self.settings["lights"][tab_name]["settings"].get("pattern", "on") + pattern_config = self.patterns.get(current_pattern, {}) + current_min_delay = pattern_config.get("min_delay", 10) + current_max_delay = pattern_config.get("max_delay", 10000) + actual_delay = slider_to_delay(slider_value, current_min_delay, current_max_delay) delay_value_label.config(text=f"{actual_delay}") delay_slider.pack(side=tk.LEFT) @@ -717,7 +1187,11 @@ class App: current_tab_widget.widgets["brightness_slider"].set(initial_settings.get("brightness", 127)) # Convert delay to slider position using logarithmic scale initial_delay = pattern_settings["delay"] - initial_delay_slider_pos = delay_to_slider(initial_delay) + # Get pattern-specific min/max delay + pattern_config = self.patterns.get(current_pattern, {}) + min_delay = pattern_config.get("min_delay", 10) + max_delay = pattern_config.get("max_delay", 10000) + initial_delay_slider_pos = delay_to_slider(initial_delay, min_delay, max_delay) current_tab_widget.widgets["delay_slider"].set(initial_delay_slider_pos) # Update the delay value label to show the actual delay value if "delay_value_label" in current_tab_widget.widgets: @@ -869,13 +1343,18 @@ class App: # but the device firmware might ignore it for these patterns. colors_to_send = tab.colors_in_palette.copy() + # Get pattern-specific min/max delay + pattern_config = self.patterns.get(current_pattern, {}) + min_delay = pattern_config.get("min_delay", 10) + max_delay = pattern_config.get("max_delay", 10000) + payload = { "save": True, # Always save this change to config "names": names, "settings": { "colors": colors_to_send, # This now dynamically changes based on pattern "brightness": tab.widgets["brightness_slider"].get(), - "delay": slider_to_delay(tab.widgets["delay_slider"].get()), # Convert from logarithmic slider + "delay": slider_to_delay(tab.widgets["delay_slider"].get(), min_delay, max_delay), # Convert from logarithmic slider "pattern": current_pattern, # Always send the current pattern "n1": int(tab.widgets["n1_var"].get()), "n2": int(tab.widgets["n2_var"].get()), @@ -886,8 +1365,12 @@ class App: # Update the settings object - save pattern-specific settings current_pattern = self.settings["lights"][selected_server]["settings"].get("pattern", "on") + # Get pattern-specific min/max delay + pattern_config = self.patterns.get(current_pattern, {}) + min_delay = pattern_config.get("min_delay", 10) + max_delay = pattern_config.get("max_delay", 10000) slider_value = tab.widgets["delay_slider"].get() - delay = slider_to_delay(slider_value) + delay = slider_to_delay(slider_value, min_delay, max_delay) n_params = {f"n{i}": int(tab.widgets[f"n{i}_var"].get()) for i in range(1, 5)} # Save pattern-specific settings @@ -934,9 +1417,16 @@ class App: @async_handler async def update_delay(self, tab): try: + # Get current pattern and its min/max delay + selected_server = self.notebook.tab(self.notebook.select(), "text") + current_pattern = self.settings["lights"][selected_server]["settings"].get("pattern", "on") + pattern_config = self.patterns.get(current_pattern, {}) + min_delay = pattern_config.get("min_delay", 10) + max_delay = pattern_config.get("max_delay", 10000) + delay_slider = tab.widgets["delay_slider"] slider_value = delay_slider.get() - delay = slider_to_delay(slider_value) # Convert from logarithmic slider to actual delay + delay = slider_to_delay(slider_value, min_delay, max_delay) # Convert from logarithmic slider to actual delay print(f"Delay: {delay}ms (slider: {slider_value})") selected_server = self.notebook.tab(self.notebook.select(), "text") @@ -1002,7 +1492,11 @@ class App: # Save current pattern's settings before switching old_pattern = current_settings_for_tab.get("pattern", "on") - current_delay = slider_to_delay(current_tab_widget.widgets["delay_slider"].get()) + # Get pattern-specific min/max delay + pattern_config = self.patterns.get(old_pattern, {}) + min_delay = pattern_config.get("min_delay", 10) + max_delay = pattern_config.get("max_delay", 10000) + current_delay = slider_to_delay(current_tab_widget.widgets["delay_slider"].get(), min_delay, max_delay) current_n_params = {f"n{i}": int(current_tab_widget.widgets[f"n{i}_var"].get()) for i in range(1, 5)} self.save_pattern_settings(tab_name, old_pattern, colors=current_tab_widget.colors_in_palette.copy(), @@ -1017,8 +1511,16 @@ class App: self.refresh_color_palette_display(current_tab_widget) # Update delay slider - delay_slider_pos = delay_to_slider(new_pattern_settings["delay"]) + # Get pattern-specific min/max delay + pattern_config = self.patterns.get(pattern_name, {}) + min_delay = pattern_config.get("min_delay", 10) + max_delay = pattern_config.get("max_delay", 10000) + delay_slider_pos = delay_to_slider(new_pattern_settings["delay"], min_delay, max_delay) current_tab_widget.widgets["delay_slider"].set(delay_slider_pos) + # Update min/max delay for the tab + if hasattr(current_tab_widget, "min_delay"): + current_tab_widget.min_delay = min_delay + current_tab_widget.max_delay = max_delay if "delay_value_label" in current_tab_widget.widgets: current_tab_widget.widgets["delay_value_label"].config(text=f"{new_pattern_settings['delay']}") @@ -1076,8 +1578,13 @@ class App: # Update settings for the current tab - save pattern-specific settings current_pattern = self.settings["lights"][selected_server]["settings"].get("pattern", "on") + # Get pattern-specific min/max delay + current_pattern = self.settings["lights"][selected_server]["settings"].get("pattern", "on") + pattern_config = self.patterns.get(current_pattern, {}) + min_delay = pattern_config.get("min_delay", 10) + max_delay = pattern_config.get("max_delay", 10000) slider_value = current_tab_widget.widgets["delay_slider"].get() - delay = slider_to_delay(slider_value) + delay = slider_to_delay(slider_value, min_delay, max_delay) n_params = {f"n{i}": int(current_tab_widget.widgets[f"n{i}_var"].get()) for i in range(1, 5)} # Save pattern-specific settings @@ -1094,6 +1601,411 @@ class App: self.settings.save() print(f"Saved settings for {selected_server}") + def get_tab_password(self): + """Get the tab modification password from settings, default to empty if not set""" + if "tab_password" not in self.settings: + self.settings["tab_password"] = "" + self.settings.save() + return self.settings.get("tab_password", "") + + def check_tab_password(self): + """Prompt for password and verify it matches the stored password""" + stored_password = self.get_tab_password() + + # If no password is set, allow access + if not stored_password: + return True + + # Create password dialog + dialog = tk.Toplevel(self.root) + dialog.title("Enter Password") + dialog.configure(bg=bg_color) + dialog.transient(self.root) + dialog.grab_set() + + # Center the dialog + dialog.geometry("400x150") + dialog.update_idletasks() + x = (dialog.winfo_screenwidth() // 2) - (dialog.winfo_width() // 2) + y = (dialog.winfo_screenheight() // 2) - (dialog.winfo_height() // 2) + dialog.geometry(f"+{x}+{y}") + + password_correct = [False] + + tk.Label(dialog, text="Enter password to modify tabs:", bg=bg_color, fg=fg_color, font=("Arial", 14)).pack(pady=15) + password_entry = tk.Entry(dialog, font=("Arial", 14), show="*", width=25) + password_entry.pack(pady=10) + password_entry.focus() + + def verify_password(): + entered_password = password_entry.get() + if entered_password == stored_password: + password_correct[0] = True + dialog.destroy() + else: + messagebox.showerror("Error", "Incorrect password", parent=dialog) + password_entry.delete(0, tk.END) + password_entry.focus() + + button_frame = tk.Frame(dialog, bg=bg_color) + button_frame.pack(pady=10) + + tk.Button( + button_frame, + text="OK", + command=verify_password, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 16), + padx=30, + pady=10, + width=8 + ).pack(side=tk.LEFT, padx=5) + + tk.Button( + button_frame, + text="Cancel", + command=dialog.destroy, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 16), + padx=30, + pady=10, + width=8 + ).pack(side=tk.LEFT, padx=5) + + password_entry.bind("", lambda e: verify_password()) + + # Wait for dialog to close + dialog.wait_window() + + return password_correct[0] + + def add_tab_dialog(self): + """Open dialog to add a new tab""" + if not self.check_tab_password(): + return + + dialog = tk.Toplevel(self.root) + dialog.title("Add New Tab") + dialog.configure(bg=bg_color) + dialog.transient(self.root) + dialog.grab_set() + + # Center the dialog + dialog.geometry("600x300") + dialog.update_idletasks() + x = (dialog.winfo_screenwidth() // 2) - (dialog.winfo_width() // 2) + y = (dialog.winfo_screenheight() // 2) - (dialog.winfo_height() // 2) + dialog.geometry(f"+{x}+{y}") + + # Tab name input + tk.Label(dialog, text="Tab Name:", bg=bg_color, fg=fg_color, font=("Arial", 12)).pack(pady=10) + name_entry = tk.Entry(dialog, font=("Arial", 12), width=30) + name_entry.pack(pady=5) + name_entry.focus() + + # IDs input + tk.Label(dialog, text="Device IDs (comma-separated):", bg=bg_color, fg=fg_color, font=("Arial", 12)).pack(pady=10) + ids_entry = tk.Entry(dialog, font=("Arial", 12), width=30) + ids_entry.pack(pady=5) + ids_entry.insert(0, "1") + + def add_tab(): + tab_name = name_entry.get().strip() + ids_str = ids_entry.get().strip() + + if not tab_name: + messagebox.showerror("Error", "Tab name cannot be empty", parent=dialog) + return + + if tab_name in self.settings["lights"]: + messagebox.showerror("Error", f"Tab '{tab_name}' already exists", parent=dialog) + return + + # Parse IDs + ids = [id.strip() for id in ids_str.split(",") if id.strip()] + if not ids: + ids = ["1"] # Default ID + + # Create new tab entry in settings + self.settings["lights"][tab_name] = { + "names": ids, + "settings": { + "pattern": "on", + "brightness": 127, + "colors": ["#000000"], + "delay": 100, + "n1": 10, + "n2": 10, + "n3": 10, + "n4": 10, + "patterns": {} + } + } + # Add to tab order + if "tab_order" not in self.settings: + self.settings["tab_order"] = [] + self.settings["tab_order"].append(tab_name) + self.save_profile() + + # Recreate tabs + self.create_tabs() + + # Select the new tab + for i, tab_text in enumerate(self.get_tab_order()): + if tab_text == tab_name: + self.notebook.select(i) + break + + dialog.destroy() + + # Buttons + button_frame = tk.Frame(dialog, bg=bg_color) + button_frame.pack(pady=20) + + tk.Button( + button_frame, + text="Add", + command=add_tab, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 18), + padx=40, + pady=15, + width=10 + ).pack(side=tk.LEFT, padx=5) + + tk.Button( + button_frame, + text="Cancel", + command=dialog.destroy, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 18), + padx=40, + pady=15, + width=10 + ).pack(side=tk.LEFT, padx=5) + + # Bind Enter key + name_entry.bind("", lambda e: add_tab()) + ids_entry.bind("", lambda e: add_tab()) + + def edit_tab_dialog(self): + """Open dialog to edit current tab""" + if not self.check_tab_password(): + return + + try: + selected_tab_name = self.notebook.tab(self.notebook.select(), "text") + except: + messagebox.showwarning("Warning", "Please select a tab to edit") + return + + if selected_tab_name not in self.settings["lights"]: + messagebox.showerror("Error", "Selected tab not found in settings") + return + + dialog = tk.Toplevel(self.root) + dialog.title("Edit Tab") + dialog.configure(bg=bg_color) + dialog.transient(self.root) + dialog.grab_set() + + # Center the dialog + dialog.geometry("600x300") + dialog.update_idletasks() + x = (dialog.winfo_screenwidth() // 2) - (dialog.winfo_width() // 2) + y = (dialog.winfo_screenheight() // 2) - (dialog.winfo_height() // 2) + dialog.geometry(f"+{x}+{y}") + + current_data = self.settings["lights"][selected_tab_name] + + # Tab name input + tk.Label(dialog, text="Tab Name:", bg=bg_color, fg=fg_color, font=("Arial", 12)).pack(pady=10) + name_entry = tk.Entry(dialog, font=("Arial", 12), width=30) + name_entry.pack(pady=5) + name_entry.insert(0, selected_tab_name) + name_entry.focus() + + # IDs input + tk.Label(dialog, text="Device IDs (comma-separated):", bg=bg_color, fg=fg_color, font=("Arial", 12)).pack(pady=10) + ids_entry = tk.Entry(dialog, font=("Arial", 12), width=30) + ids_entry.pack(pady=5) + ids_entry.insert(0, ", ".join(current_data["names"])) + + def update_tab(): + new_tab_name = name_entry.get().strip() + ids_str = ids_entry.get().strip() + + if not new_tab_name: + messagebox.showerror("Error", "Tab name cannot be empty", parent=dialog) + return + + # Parse IDs + ids = [id.strip() for id in ids_str.split(",") if id.strip()] + if not ids: + ids = ["1"] # Default ID + + # If name changed, rename the tab + if new_tab_name != selected_tab_name: + if new_tab_name in self.settings["lights"]: + messagebox.showerror("Error", f"Tab '{new_tab_name}' already exists", parent=dialog) + return + + # Rename in settings + self.settings["lights"][new_tab_name] = self.settings["lights"][selected_tab_name] + del self.settings["lights"][selected_tab_name] + + # Update tab order if name changed + if "tab_order" in self.settings and selected_tab_name in self.settings["tab_order"]: + index = self.settings["tab_order"].index(selected_tab_name) + self.settings["tab_order"][index] = new_tab_name + + # Update IDs + self.settings["lights"][new_tab_name]["names"] = ids + self.save_profile() + + # Recreate tabs + self.create_tabs() + + # Select the edited tab + for i, tab_text in enumerate(self.get_tab_order()): + if tab_text == new_tab_name: + self.notebook.select(i) + break + + dialog.destroy() + + # Buttons + button_frame = tk.Frame(dialog, bg=bg_color) + button_frame.pack(pady=20) + + tk.Button( + button_frame, + text="Update", + command=update_tab, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 18), + padx=40, + pady=15, + width=10 + ).pack(side=tk.LEFT, padx=5) + + tk.Button( + button_frame, + text="Cancel", + command=dialog.destroy, + bg=active_bg_color, + fg=fg_color, + font=("Arial", 18), + padx=40, + pady=15, + width=10 + ).pack(side=tk.LEFT, padx=5) + + # Bind Enter key + name_entry.bind("", lambda e: update_tab()) + ids_entry.bind("", lambda e: update_tab()) + + def delete_tab_dialog(self): + """Delete the current tab with confirmation""" + if not self.check_tab_password(): + return + + try: + selected_tab_name = self.notebook.tab(self.notebook.select(), "text") + except: + messagebox.showwarning("Warning", "Please select a tab to delete") + return + + if selected_tab_name not in self.settings["lights"]: + messagebox.showerror("Error", "Selected tab not found in settings") + return + + # Confirmation dialog + result = messagebox.askyesno( + "Confirm Delete", + f"Are you sure you want to delete the tab '{selected_tab_name}'?\n\nThis action cannot be undone.", + icon="warning" + ) + + if result: + # Delete from settings + del self.settings["lights"][selected_tab_name] + + # Remove from tab order + if "tab_order" in self.settings and selected_tab_name in self.settings["tab_order"]: + self.settings["tab_order"].remove(selected_tab_name) + + self.save_profile() + + # Recreate tabs + self.create_tabs() + + # Select first tab if available + if self.settings["lights"]: + self.notebook.select(0) + + def move_tab_left(self): + """Move the current tab left in the order""" + try: + selected_index = self.notebook.index(self.notebook.select()) + except: + messagebox.showwarning("Warning", "Please select a tab to move") + return + + if selected_index == 0: + messagebox.showinfo("Info", "Tab is already at the leftmost position") + return + + # Get current tab order + tab_order = self.get_tab_order() + + # Swap with previous tab + tab_order[selected_index], tab_order[selected_index - 1] = tab_order[selected_index - 1], tab_order[selected_index] + + # Save new order + self.settings["tab_order"] = tab_order + self.save_profile() + + # Recreate tabs with new order + self.create_tabs() + + # Select the moved tab (now at previous index) + self.notebook.select(selected_index - 1) + + def move_tab_right(self): + """Move the current tab right in the order""" + try: + selected_index = self.notebook.index(self.notebook.select()) + except: + messagebox.showwarning("Warning", "Please select a tab to move") + return + + total_tabs = self.notebook.index("end") + if selected_index >= total_tabs - 1: + messagebox.showinfo("Info", "Tab is already at the rightmost position") + return + + # Get current tab order + tab_order = self.get_tab_order() + + # Swap with next tab + tab_order[selected_index], tab_order[selected_index + 1] = tab_order[selected_index + 1], tab_order[selected_index] + + # Save new order + self.settings["tab_order"] = tab_order + self.save_profile() + + # Recreate tabs with new order + self.create_tabs() + + # Select the moved tab (now at next index) + self.notebook.select(selected_index + 1) + if __name__ == "__main__": app = App() diff --git a/src/settings.py b/src/settings.py index 571cd3b..89c167e 100644 --- a/src/settings.py +++ b/src/settings.py @@ -9,7 +9,9 @@ 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) + settings_to_save = {k: v for k, v in self.items() if k not in ["lights", "tab_order"]} + j = json.dumps(settings_to_save, indent=4) with open(self.SETTINGS_FILE, 'w') as file: file.write(j) print("Settings saved successfully.")