瀏覽代碼

add DW touch led control

tuanchris 2 月之前
父節點
當前提交
4e492e4e5c

+ 1 - 1
VERSION

@@ -1 +1 @@
-3.4.5
+3.5.0

+ 10 - 0
dune-weaver-touch/.env.example

@@ -0,0 +1,10 @@
+# Dune Weaver Touch Configuration
+# Copy this file to .env and modify as needed
+
+# URL of the Dune Weaver server
+# Default: http://localhost:8080
+# Examples:
+#   DUNE_WEAVER_URL=http://localhost:8080
+#   DUNE_WEAVER_URL=http://dwmp.local:8080
+#   DUNE_WEAVER_URL=http://192.168.1.100:8080
+DUNE_WEAVER_URL=http://localhost:8080

+ 342 - 9
dune-weaver-touch/backend.py

@@ -44,10 +44,16 @@ class Backend(QObject):
     PAUSE_OPTIONS = {
         "0s": 0,        # No pause
         "1 min": 60,    # 1 minute
-        "5 min": 300,   # 5 minutes  
+        "5 min": 300,   # 5 minutes
         "15 min": 900,  # 15 minutes
         "30 min": 1800, # 30 minutes
-        "1 hour": 3600  # 1 hour
+        "1 hour": 3600, # 1 hour
+        "2 hour": 7200, # 2 hours
+        "3 hour": 10800, # 3 hours
+        "4 hour": 14400, # 4 hours
+        "5 hour": 18000, # 5 hours
+        "6 hour": 21600, # 6 hours
+        "12 hour": 43200 # 12 hours
     }
     
     # Signals
@@ -65,14 +71,20 @@ class Backend(QObject):
     screenStateChanged = Signal(bool)  # True = on, False = off
     screenTimeoutChanged = Signal(int)  # New signal for timeout changes
     pauseBetweenPatternsChanged = Signal(int)  # New signal for pause changes
-    
+
     # Backend connection status signals
     backendConnectionChanged = Signal(bool)  # True = backend reachable, False = unreachable
     reconnectStatusChanged = Signal(str)  # Current reconnection status message
+
+    # LED control signals
+    ledStatusChanged = Signal()
+    ledEffectsLoaded = Signal(list)  # List of available effects
+    ledPalettesLoaded = Signal(list)  # List of available palettes
     
     def __init__(self):
         super().__init__()
-        self.base_url = "http://localhost:8080"
+        # Load base URL from environment variable, default to localhost
+        self.base_url = os.environ.get("DUNE_WEAVER_URL", "http://localhost:8080")
         
         # Initialize all status properties first
         self._current_file = ""
@@ -89,6 +101,17 @@ class Backend(QObject):
         # Backend connection status
         self._backend_connected = False
         self._reconnect_status = "Connecting to backend..."
+
+        # LED control state
+        self._led_provider = "none"  # "none", "wled", or "dw_leds"
+        self._led_connected = False
+        self._led_power_on = False
+        self._led_brightness = 100
+        self._led_effects = []
+        self._led_palettes = []
+        self._led_current_effect = 0
+        self._led_current_palette = 0
+        self._led_color = "#ffffff"
         
         # WebSocket for status with reconnection
         self.ws = QWebSocket()
@@ -209,9 +232,11 @@ class Backend(QObject):
         self.connectionChanged.emit()
         self.backendConnectionChanged.emit(True)
         self.reconnectStatusChanged.emit("Connected to backend")
-        
+
         # Load initial settings when we connect
         self.loadControlSettings()
+        # Also load LED config automatically
+        self.loadLedConfig()
     
     @Slot()
     def _on_ws_disconnected(self):
@@ -263,8 +288,9 @@ class Backend(QObject):
         if self.ws.state() != QAbstractSocket.SocketState.UnconnectedState:
             self.ws.close()
         
-        # Attempt new connection
-        self.ws.open("ws://localhost:8080/ws/status")
+        # Attempt new connection - derive WebSocket URL from base URL
+        ws_url = self.base_url.replace("http://", "ws://").replace("https://", "wss://") + "/ws/status"
+        self.ws.open(ws_url)
     
     @Slot()
     def retryConnection(self):
@@ -1235,5 +1261,312 @@ class Backend(QObject):
                 
         except Exception as e:
             print(f"❌ Error monitoring touch input: {e}")
-        
-        print("👆 Touch monitoring stopped")
+
+        print("👆 Touch monitoring stopped")
+
+    # ==================== LED Control Methods ====================
+
+    # LED Properties
+    @Property(str, notify=ledStatusChanged)
+    def ledProvider(self):
+        return self._led_provider
+
+    @Property(bool, notify=ledStatusChanged)
+    def ledConnected(self):
+        return self._led_connected
+
+    @Property(bool, notify=ledStatusChanged)
+    def ledPowerOn(self):
+        return self._led_power_on
+
+    @Property(int, notify=ledStatusChanged)
+    def ledBrightness(self):
+        return self._led_brightness
+
+    @Property(list, notify=ledEffectsLoaded)
+    def ledEffects(self):
+        return self._led_effects
+
+    @Property(list, notify=ledPalettesLoaded)
+    def ledPalettes(self):
+        return self._led_palettes
+
+    @Property(int, notify=ledStatusChanged)
+    def ledCurrentEffect(self):
+        return self._led_current_effect
+
+    @Property(int, notify=ledStatusChanged)
+    def ledCurrentPalette(self):
+        return self._led_current_palette
+
+    @Property(str, notify=ledStatusChanged)
+    def ledColor(self):
+        return self._led_color
+
+    @Slot()
+    def loadLedConfig(self):
+        """Load LED configuration from the server"""
+        print("💡 Loading LED configuration...")
+        asyncio.create_task(self._load_led_config())
+
+    async def _load_led_config(self):
+        if not self.session:
+            print("⚠️ Session not ready for LED config")
+            return
+
+        try:
+            timeout = aiohttp.ClientTimeout(total=5)
+            async with self.session.get(f"{self.base_url}/get_led_config", timeout=timeout) as resp:
+                if resp.status == 200:
+                    data = await resp.json()
+                    self._led_provider = data.get("provider", "none")
+                    print(f"💡 LED provider: {self._led_provider}")
+
+                    if self._led_provider == "dw_leds":
+                        # Load DW LEDs status
+                        await self._load_led_status()
+                        await self._load_led_effects()
+                        await self._load_led_palettes()
+
+                    self.ledStatusChanged.emit()
+                else:
+                    print(f"❌ Failed to get LED config: {resp.status}")
+        except Exception as e:
+            print(f"💥 Exception loading LED config: {e}")
+
+    async def _load_led_status(self):
+        """Load current LED status"""
+        if not self.session:
+            return
+
+        try:
+            timeout = aiohttp.ClientTimeout(total=5)
+            async with self.session.get(f"{self.base_url}/api/dw_leds/status", timeout=timeout) as resp:
+                if resp.status == 200:
+                    data = await resp.json()
+                    self._led_connected = data.get("connected", False)
+                    self._led_power_on = data.get("power_on", False)
+                    self._led_brightness = data.get("brightness", 100)
+                    self._led_current_effect = data.get("current_effect", 0)
+                    self._led_current_palette = data.get("current_palette", 0)
+                    print(f"💡 LED status: connected={self._led_connected}, power={self._led_power_on}, brightness={self._led_brightness}")
+                    self.ledStatusChanged.emit()
+        except Exception as e:
+            print(f"💥 Exception loading LED status: {e}")
+
+    async def _load_led_effects(self):
+        """Load available LED effects"""
+        if not self.session:
+            return
+
+        try:
+            timeout = aiohttp.ClientTimeout(total=5)
+            async with self.session.get(f"{self.base_url}/api/dw_leds/effects", timeout=timeout) as resp:
+                if resp.status == 200:
+                    data = await resp.json()
+                    # API returns effects as [[id, name], ...] arrays
+                    raw_effects = data.get("effects", [])
+                    # Convert to list of dicts for easier use in QML
+                    self._led_effects = [{"id": e[0], "name": e[1]} for e in raw_effects if len(e) >= 2]
+                    print(f"💡 Loaded {len(self._led_effects)} LED effects")
+                    self.ledEffectsLoaded.emit(self._led_effects)
+        except Exception as e:
+            print(f"💥 Exception loading LED effects: {e}")
+
+    async def _load_led_palettes(self):
+        """Load available LED palettes"""
+        if not self.session:
+            return
+
+        try:
+            timeout = aiohttp.ClientTimeout(total=5)
+            async with self.session.get(f"{self.base_url}/api/dw_leds/palettes", timeout=timeout) as resp:
+                if resp.status == 200:
+                    data = await resp.json()
+                    # API returns palettes as [[id, name], ...] arrays
+                    raw_palettes = data.get("palettes", [])
+                    # Convert to list of dicts for easier use in QML
+                    self._led_palettes = [{"id": p[0], "name": p[1]} for p in raw_palettes if len(p) >= 2]
+                    print(f"💡 Loaded {len(self._led_palettes)} LED palettes")
+                    self.ledPalettesLoaded.emit(self._led_palettes)
+        except Exception as e:
+            print(f"💥 Exception loading LED palettes: {e}")
+
+    @Slot()
+    def refreshLedStatus(self):
+        """Refresh LED status from server"""
+        print("💡 Refreshing LED status...")
+        asyncio.create_task(self._load_led_status())
+
+    @Slot()
+    def toggleLedPower(self):
+        """Toggle LED power on/off"""
+        print("💡 Toggling LED power...")
+        asyncio.create_task(self._toggle_led_power())
+
+    async def _toggle_led_power(self):
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+
+        try:
+            async with self.session.post(
+                f"{self.base_url}/api/dw_leds/power",
+                json={"state": 2}  # Toggle
+            ) as resp:
+                if resp.status == 200:
+                    data = await resp.json()
+                    self._led_power_on = data.get("power_on", False)
+                    self._led_connected = data.get("connected", False)
+                    print(f"💡 LED power toggled: {self._led_power_on}")
+                    self.ledStatusChanged.emit()
+                else:
+                    self.errorOccurred.emit(f"Failed to toggle LED power: {resp.status}")
+        except Exception as e:
+            print(f"💥 Exception toggling LED power: {e}")
+            self.errorOccurred.emit(str(e))
+
+    @Slot(bool)
+    def setLedPower(self, on):
+        """Set LED power state (True=on, False=off)"""
+        print(f"💡 Setting LED power: {on}")
+        asyncio.create_task(self._set_led_power(on))
+
+    async def _set_led_power(self, on):
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+
+        try:
+            async with self.session.post(
+                f"{self.base_url}/api/dw_leds/power",
+                json={"state": 1 if on else 0}
+            ) as resp:
+                if resp.status == 200:
+                    data = await resp.json()
+                    self._led_power_on = data.get("power_on", False)
+                    self._led_connected = data.get("connected", False)
+                    print(f"💡 LED power set: {self._led_power_on}")
+                    self.ledStatusChanged.emit()
+                else:
+                    self.errorOccurred.emit(f"Failed to set LED power: {resp.status}")
+        except Exception as e:
+            print(f"💥 Exception setting LED power: {e}")
+            self.errorOccurred.emit(str(e))
+
+    @Slot(int)
+    def setLedBrightness(self, value):
+        """Set LED brightness (0-100)"""
+        print(f"💡 Setting LED brightness: {value}")
+        asyncio.create_task(self._set_led_brightness(value))
+
+    async def _set_led_brightness(self, value):
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+
+        try:
+            async with self.session.post(
+                f"{self.base_url}/api/dw_leds/brightness",
+                json={"value": value}
+            ) as resp:
+                if resp.status == 200:
+                    self._led_brightness = value
+                    print(f"💡 LED brightness set: {value}")
+                    self.ledStatusChanged.emit()
+                else:
+                    self.errorOccurred.emit(f"Failed to set brightness: {resp.status}")
+        except Exception as e:
+            print(f"💥 Exception setting LED brightness: {e}")
+            self.errorOccurred.emit(str(e))
+
+    @Slot(int, int, int)
+    def setLedColor(self, r, g, b):
+        """Set LED color using RGB values"""
+        print(f"💡 Setting LED color: RGB({r}, {g}, {b})")
+        asyncio.create_task(self._set_led_color(r, g, b))
+
+    async def _set_led_color(self, r, g, b):
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+
+        try:
+            async with self.session.post(
+                f"{self.base_url}/api/dw_leds/color",
+                json={"color": [r, g, b]}
+            ) as resp:
+                if resp.status == 200:
+                    self._led_color = f"#{r:02x}{g:02x}{b:02x}"
+                    print(f"💡 LED color set: {self._led_color}")
+                    self.ledStatusChanged.emit()
+                else:
+                    self.errorOccurred.emit(f"Failed to set color: {resp.status}")
+        except Exception as e:
+            print(f"💥 Exception setting LED color: {e}")
+            self.errorOccurred.emit(str(e))
+
+    @Slot(str)
+    def setLedColorHex(self, hexColor):
+        """Set LED color using hex string (e.g., '#ff0000')"""
+        # Parse hex color
+        hexColor = hexColor.lstrip('#')
+        if len(hexColor) == 6:
+            r = int(hexColor[0:2], 16)
+            g = int(hexColor[2:4], 16)
+            b = int(hexColor[4:6], 16)
+            self.setLedColor(r, g, b)
+        else:
+            print(f"⚠️ Invalid hex color: {hexColor}")
+
+    @Slot(int)
+    def setLedEffect(self, effectId):
+        """Set LED effect by ID"""
+        print(f"💡 Setting LED effect: {effectId}")
+        asyncio.create_task(self._set_led_effect(effectId))
+
+    async def _set_led_effect(self, effectId):
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+
+        try:
+            async with self.session.post(
+                f"{self.base_url}/api/dw_leds/effect",
+                json={"effect_id": effectId}
+            ) as resp:
+                if resp.status == 200:
+                    self._led_current_effect = effectId
+                    print(f"💡 LED effect set: {effectId}")
+                    self.ledStatusChanged.emit()
+                else:
+                    self.errorOccurred.emit(f"Failed to set effect: {resp.status}")
+        except Exception as e:
+            print(f"💥 Exception setting LED effect: {e}")
+            self.errorOccurred.emit(str(e))
+
+    @Slot(int)
+    def setLedPalette(self, paletteId):
+        """Set LED palette by ID"""
+        print(f"💡 Setting LED palette: {paletteId}")
+        asyncio.create_task(self._set_led_palette(paletteId))
+
+    async def _set_led_palette(self, paletteId):
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+
+        try:
+            async with self.session.post(
+                f"{self.base_url}/api/dw_leds/palette",
+                json={"palette_id": paletteId}
+            ) as resp:
+                if resp.status == 200:
+                    self._led_current_palette = paletteId
+                    print(f"💡 LED palette set: {paletteId}")
+                    self.ledStatusChanged.emit()
+                else:
+                    self.errorOccurred.emit(f"Failed to set palette: {resp.status}")
+        except Exception as e:
+            print(f"💥 Exception setting LED palette: {e}")
+            self.errorOccurred.emit(str(e))

+ 4 - 0
dune-weaver-touch/main.py

@@ -10,6 +10,10 @@ from PySide6.QtGui import QGuiApplication, QTouchEvent, QMouseEvent
 from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType
 from qasync import QEventLoop
 
+# Load environment variables from .env file if it exists
+from dotenv import load_dotenv
+load_dotenv(Path(__file__).parent / ".env")
+
 from backend import Backend
 from models.pattern_model import PatternModel
 from models.playlist_model import PlaylistModel

+ 1 - 0
dune-weaver-touch/qml/components/BottomNavTab.qml

@@ -40,6 +40,7 @@ Rectangle {
                     case "list_alt": return "☰"    // U+2630 - Hamburger menu, widely supported
                     case "table_chart": return "⚙"  // U+2699 - Gear without variant selector
                     case "play_arrow": return "▶"   // U+25B6 - Play without variant selector
+                    case "lightbulb": return "☀"   // U+2600 - Sun symbol for LED
                     default: {
                         console.log("Unknown icon:", iconValue, "- using default")
                         return "□"  // U+25A1 - Simple box, universally supported

+ 14 - 4
dune-weaver-touch/qml/components/BottomNavigation.qml

@@ -53,15 +53,25 @@ Rectangle {
             active: bottomNav.currentIndex === 2
             onClicked: bottomNav.tabClicked(2)
         }
-        
-        // Execution Tab
+
+        // LED Control Tab (index 3)
         BottomNavTab {
             Layout.fillWidth: true
             Layout.fillHeight: true
-            icon: "play_arrow"
-            text: "Execution"
+            icon: "lightbulb"
+            text: "LED"
             active: bottomNav.currentIndex === 3
             onClicked: bottomNav.tabClicked(3)
         }
+
+        // Execution Tab (index 4)
+        BottomNavTab {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            icon: "play_arrow"
+            text: "Execution"
+            active: bottomNav.currentIndex === 4
+            onClicked: bottomNav.tabClicked(4)
+        }
     }
 }

+ 15 - 7
dune-weaver-touch/qml/main.qml

@@ -28,16 +28,16 @@ ApplicationWindow {
         if (shouldNavigateToExecution) {
             console.log("🎯 Navigating to execution page")
             console.log("🎯 Current stack depth:", stackView.depth)
-            
+
             // If we're in a sub-page (like PatternDetailPage), pop back to main view first
             if (stackView.depth > 1) {
                 console.log("🎯 Popping back to main view first")
                 stackView.pop()
             }
-            
-            // Then navigate to ExecutionPage tab
-            console.log("🎯 Setting currentPageIndex to 3")
-            currentPageIndex = 3
+
+            // Then navigate to ExecutionPage tab (index 4)
+            console.log("🎯 Setting currentPageIndex to 4")
+            currentPageIndex = 4
             shouldNavigateToExecution = false
         }
     }
@@ -167,8 +167,16 @@ ApplicationWindow {
                             item.backend = backend
                         }
                     }
-                    
-                    // Execution Page
+
+                    // LED Control Page (index 3)
+                    Loader {
+                        source: "pages/LedControlPage.qml"
+                        onLoaded: {
+                            item.backend = backend
+                        }
+                    }
+
+                    // Execution Page (index 4)
                     Loader {
                         source: "pages/ExecutionPage.qml"
                         onLoaded: {

+ 550 - 0
dune-weaver-touch/qml/pages/LedControlPage.qml

@@ -0,0 +1,550 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import QtQuick.Dialogs
+import "../components"
+import "../components" as Components
+
+Page {
+    id: page
+    property var backend: null
+
+    // Local state
+    property bool ledPowerOn: false
+    property int ledBrightness: 100
+    property string ledProvider: "none"
+    property bool ledConnected: false
+    property int currentEffectIndex: 0
+    property int currentPaletteIndex: 0
+    property var effectsList: []
+    property var palettesList: []
+
+    // Predefined colors for quick selection (muted tones to fit dark UI)
+    property var presetColors: [
+        {"name": "White", "color": "#e8e8e8", "sendColor": "#ffffff"},
+        {"name": "Warm", "color": "#d4a574", "sendColor": "#ffaa55"},
+        {"name": "Red", "color": "#c45c5c", "sendColor": "#ff0000"},
+        {"name": "Orange", "color": "#d4875c", "sendColor": "#ff8800"},
+        {"name": "Yellow", "color": "#c9b95c", "sendColor": "#ffff00"},
+        {"name": "Green", "color": "#5cb85c", "sendColor": "#00ff00"},
+        {"name": "Cyan", "color": "#5cb8b8", "sendColor": "#00ffff"},
+        {"name": "Blue", "color": "#5c7cc4", "sendColor": "#0000ff"},
+        {"name": "Purple", "color": "#8b5cc4", "sendColor": "#8800ff"},
+        {"name": "Pink", "color": "#c45c99", "sendColor": "#ff00ff"}
+    ]
+
+    // Backend signal connections
+    Connections {
+        target: backend
+
+        function onLedStatusChanged() {
+            if (backend) {
+                ledPowerOn = backend.ledPowerOn
+                ledBrightness = backend.ledBrightness
+                ledProvider = backend.ledProvider
+                ledConnected = backend.ledConnected
+                currentEffectIndex = backend.ledCurrentEffect
+                currentPaletteIndex = backend.ledCurrentPalette
+            }
+        }
+
+        function onLedEffectsLoaded(effects) {
+            effectsList = effects
+        }
+
+        function onLedPalettesLoaded(palettes) {
+            palettesList = palettes
+        }
+    }
+
+    // Load LED config on page load
+    Component.onCompleted: {
+        if (backend) {
+            backend.loadLedConfig()
+        }
+    }
+
+    Rectangle {
+        anchors.fill: parent
+        color: Components.ThemeManager.backgroundColor
+    }
+
+    ColumnLayout {
+        anchors.fill: parent
+        spacing: 0
+
+        // Header
+        Rectangle {
+            Layout.fillWidth: true
+            Layout.preferredHeight: 50
+            color: Components.ThemeManager.surfaceColor
+
+            Rectangle {
+                anchors.bottom: parent.bottom
+                width: parent.width
+                height: 1
+                color: Components.ThemeManager.borderColor
+            }
+
+            RowLayout {
+                anchors.fill: parent
+                anchors.leftMargin: 15
+                anchors.rightMargin: 10
+
+                ConnectionStatus {
+                    backend: page.backend
+                    Layout.rightMargin: 8
+                }
+
+                Label {
+                    text: "LED Control"
+                    font.pixelSize: 18
+                    font.bold: true
+                    color: Components.ThemeManager.textPrimary
+                }
+
+                Item {
+                    Layout.fillWidth: true
+                }
+            }
+        }
+
+        // Main Content
+        ScrollView {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            contentWidth: availableWidth
+
+            ColumnLayout {
+                width: parent.width
+                anchors.margins: 5
+                spacing: 2
+
+                // Provider Info & Power/Brightness Section
+                Rectangle {
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: ledProvider === "none" ? 100 : (ledProvider === "wled" ? 90 : 110)
+                    Layout.margins: 5
+                    radius: 8
+                    color: Components.ThemeManager.surfaceColor
+
+                    ColumnLayout {
+                        anchors.fill: parent
+                        anchors.margins: 15
+                        spacing: 10
+
+                        // Not configured message
+                        ColumnLayout {
+                            visible: ledProvider === "none"
+                            Layout.fillWidth: true
+                            spacing: 8
+
+                            Label {
+                                text: "LED Not Configured"
+                                font.pixelSize: 14
+                                font.bold: true
+                                color: Components.ThemeManager.textPrimary
+                            }
+
+                            Label {
+                                text: "Configure LED settings in the main Dune Weaver web interface"
+                                font.pixelSize: 12
+                                color: Components.ThemeManager.textSecondary
+                                wrapMode: Text.WordWrap
+                                Layout.fillWidth: true
+                            }
+                        }
+
+                        // DW LEDs Controls - Power and Brightness in same section
+                        ColumnLayout {
+                            visible: ledProvider === "dw_leds"
+                            Layout.fillWidth: true
+                            spacing: 8
+
+                            // Power row with status and toggle
+                            RowLayout {
+                                Layout.fillWidth: true
+                                spacing: 12
+
+                                // Status indicator with label
+                                RowLayout {
+                                    spacing: 6
+
+                                    Rectangle {
+                                        width: 12
+                                        height: 12
+                                        radius: 6
+                                        color: ledPowerOn ? "#4CAF50" : "#6b7280"
+                                    }
+
+                                    Label {
+                                        text: ledPowerOn ? "On" : "Off"
+                                        font.pixelSize: 13
+                                        font.bold: true
+                                        color: Components.ThemeManager.textPrimary
+                                    }
+                                }
+
+                                // Toggle button
+                                ModernControlButton {
+                                    Layout.preferredWidth: 100
+                                    Layout.preferredHeight: 36
+                                    text: ledPowerOn ? "Turn Off" : "Turn On"
+                                    icon: ""
+                                    buttonColor: ledPowerOn ? "#6b7280" : "#4CAF50"
+                                    fontSize: 11
+
+                                    onClicked: {
+                                        if (backend) {
+                                            backend.toggleLedPower()
+                                        }
+                                    }
+                                }
+
+                                Item { Layout.fillWidth: true }
+
+                                // Connection status (smaller, secondary)
+                                RowLayout {
+                                    spacing: 4
+
+                                    Rectangle {
+                                        width: 8
+                                        height: 8
+                                        radius: 4
+                                        color: ledConnected ? "#4CAF50" : "#ef4444"
+                                    }
+
+                                    Label {
+                                        text: ledConnected ? "Connected" : "Disconnected"
+                                        font.pixelSize: 10
+                                        color: Components.ThemeManager.textTertiary
+                                    }
+                                }
+                            }
+
+                            // Brightness row
+                            RowLayout {
+                                Layout.fillWidth: true
+                                spacing: 10
+
+                                Label {
+                                    text: "Brightness"
+                                    font.pixelSize: 12
+                                    color: Components.ThemeManager.textSecondary
+                                }
+
+                                Slider {
+                                    id: brightnessSlider
+                                    Layout.fillWidth: true
+                                    from: 0
+                                    to: 100
+                                    stepSize: 5
+                                    value: ledBrightness
+
+                                    onMoved: {
+                                        if (backend) {
+                                            backend.setLedBrightness(Math.round(value))
+                                        }
+                                    }
+                                }
+
+                                Label {
+                                    text: Math.round(brightnessSlider.value) + "%"
+                                    font.pixelSize: 12
+                                    font.bold: true
+                                    color: Components.ThemeManager.textPrimary
+                                    Layout.preferredWidth: 35
+                                    horizontalAlignment: Text.AlignRight
+                                }
+                            }
+                        }
+
+                        // WLED Info
+                        ColumnLayout {
+                            visible: ledProvider === "wled"
+                            Layout.fillWidth: true
+                            spacing: 8
+
+                            Label {
+                                text: "WLED Mode"
+                                font.pixelSize: 14
+                                font.bold: true
+                                color: Components.ThemeManager.textPrimary
+                            }
+
+                            Label {
+                                text: "Use the main Dune Weaver web interface for WLED controls"
+                                font.pixelSize: 12
+                                color: Components.ThemeManager.textSecondary
+                                wrapMode: Text.WordWrap
+                                Layout.fillWidth: true
+                            }
+                        }
+                    }
+                }
+
+                // Effects Section (only for dw_leds)
+                Rectangle {
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: effectsList.length > 0 ? 180 : 80
+                    Layout.margins: 5
+                    radius: 8
+                    color: Components.ThemeManager.surfaceColor
+                    visible: ledProvider === "dw_leds"
+
+                    ColumnLayout {
+                        anchors.fill: parent
+                        anchors.margins: 15
+                        spacing: 10
+
+                        Label {
+                            text: "Effects"
+                            font.pixelSize: 14
+                            font.bold: true
+                            color: Components.ThemeManager.textPrimary
+                        }
+
+                        // Show loading or no effects message
+                        Label {
+                            visible: effectsList.length === 0
+                            text: "No effects available"
+                            font.pixelSize: 12
+                            color: Components.ThemeManager.textSecondary
+                        }
+
+                        // Effects grid
+                        GridLayout {
+                            Layout.fillWidth: true
+                            columns: 4
+                            rowSpacing: 6
+                            columnSpacing: 6
+                            visible: effectsList.length > 0
+
+                            Repeater {
+                                model: effectsList.slice(0, 12) // Show first 12 effects
+
+                                Rectangle {
+                                    property int effectId: modelData.id !== undefined ? modelData.id : index
+                                    property bool isSelected: effectId === currentEffectIndex
+
+                                    Layout.fillWidth: true
+                                    Layout.preferredHeight: 35
+                                    radius: 6
+                                    color: isSelected ?
+                                           Components.ThemeManager.selectedBackground :
+                                           Components.ThemeManager.buttonBackground
+                                    border.color: isSelected ?
+                                                  Components.ThemeManager.selectedBorder :
+                                                  Components.ThemeManager.buttonBorder
+                                    border.width: 1
+
+                                    Label {
+                                        anchors.centerIn: parent
+                                        anchors.leftMargin: 4
+                                        anchors.rightMargin: 4
+                                        width: parent.width - 8
+                                        text: modelData.name || ("Effect " + effectId)
+                                        font.pixelSize: 10
+                                        color: isSelected ? "white" : Components.ThemeManager.textPrimary
+                                        elide: Text.ElideRight
+                                        horizontalAlignment: Text.AlignHCenter
+                                    }
+
+                                    MouseArea {
+                                        anchors.fill: parent
+                                        onClicked: {
+                                            if (backend) {
+                                                backend.setLedEffect(effectId)
+                                                currentEffectIndex = effectId
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+
+                // Palettes Section (only for dw_leds)
+                Rectangle {
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: palettesList.length > 0 ? 140 : 80
+                    Layout.margins: 5
+                    radius: 8
+                    color: Components.ThemeManager.surfaceColor
+                    visible: ledProvider === "dw_leds"
+
+                    ColumnLayout {
+                        anchors.fill: parent
+                        anchors.margins: 15
+                        spacing: 10
+
+                        Label {
+                            text: "Palettes"
+                            font.pixelSize: 14
+                            font.bold: true
+                            color: Components.ThemeManager.textPrimary
+                        }
+
+                        // Show loading or no palettes message
+                        Label {
+                            visible: palettesList.length === 0
+                            text: "No palettes available"
+                            font.pixelSize: 12
+                            color: Components.ThemeManager.textSecondary
+                        }
+
+                        // Palettes grid
+                        GridLayout {
+                            Layout.fillWidth: true
+                            columns: 4
+                            rowSpacing: 6
+                            columnSpacing: 6
+                            visible: palettesList.length > 0
+
+                            Repeater {
+                                model: palettesList.slice(0, 8) // Show first 8 palettes
+
+                                Rectangle {
+                                    property int paletteId: modelData.id !== undefined ? modelData.id : index
+                                    property bool isSelected: paletteId === currentPaletteIndex
+
+                                    Layout.fillWidth: true
+                                    Layout.preferredHeight: 35
+                                    radius: 6
+                                    color: isSelected ?
+                                           Components.ThemeManager.selectedBackground :
+                                           Components.ThemeManager.buttonBackground
+                                    border.color: isSelected ?
+                                                  Components.ThemeManager.selectedBorder :
+                                                  Components.ThemeManager.buttonBorder
+                                    border.width: 1
+
+                                    Label {
+                                        anchors.centerIn: parent
+                                        anchors.leftMargin: 4
+                                        anchors.rightMargin: 4
+                                        width: parent.width - 8
+                                        text: modelData.name || ("Palette " + paletteId)
+                                        font.pixelSize: 10
+                                        color: isSelected ? "white" : Components.ThemeManager.textPrimary
+                                        elide: Text.ElideRight
+                                        horizontalAlignment: Text.AlignHCenter
+                                    }
+
+                                    MouseArea {
+                                        anchors.fill: parent
+                                        onClicked: {
+                                            if (backend) {
+                                                backend.setLedPalette(paletteId)
+                                                currentPaletteIndex = paletteId
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+
+                // Quick Colors Section - MOVED TO BOTTOM (only for dw_leds)
+                Rectangle {
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: 160
+                    Layout.margins: 5
+                    radius: 8
+                    color: Components.ThemeManager.surfaceColor
+                    visible: ledProvider === "dw_leds"
+
+                    ColumnLayout {
+                        anchors.fill: parent
+                        anchors.margins: 15
+                        spacing: 10
+
+                        Label {
+                            text: "Quick Colors"
+                            font.pixelSize: 14
+                            font.bold: true
+                            color: Components.ThemeManager.textPrimary
+                        }
+
+                        GridLayout {
+                            Layout.fillWidth: true
+                            Layout.fillHeight: true
+                            columns: 5
+                            rowSpacing: 8
+                            columnSpacing: 8
+
+                            Repeater {
+                                model: presetColors
+
+                                Rectangle {
+                                    Layout.fillWidth: true
+                                    Layout.fillHeight: true
+                                    Layout.minimumHeight: 50
+                                    radius: 6
+                                    color: Components.ThemeManager.buttonBackground
+                                    border.color: Components.ThemeManager.buttonBorder
+                                    border.width: 1
+
+                                    RowLayout {
+                                        anchors.centerIn: parent
+                                        spacing: 6
+
+                                        // Color indicator circle
+                                        Rectangle {
+                                            width: 14
+                                            height: 14
+                                            radius: 7
+                                            color: modelData.color
+                                            border.color: Qt.darker(modelData.color, 1.2)
+                                            border.width: 1
+                                        }
+
+                                        Label {
+                                            text: modelData.name
+                                            font.pixelSize: 11
+                                            color: Components.ThemeManager.textPrimary
+                                        }
+                                    }
+
+                                    MouseArea {
+                                        anchors.fill: parent
+                                        onClicked: {
+                                            if (backend) {
+                                                backend.setLedColorHex(modelData.sendColor)
+                                            }
+                                        }
+                                    }
+
+                                    // Touch feedback
+                                    Rectangle {
+                                        id: colorTouchFeedback
+                                        anchors.fill: parent
+                                        color: Components.ThemeManager.darkMode ? "#ffffff" : "#000000"
+                                        opacity: 0
+                                        radius: 6
+
+                                        NumberAnimation {
+                                            id: colorTouchAnimation
+                                            target: colorTouchFeedback
+                                            property: "opacity"
+                                            from: 0.15
+                                            to: 0
+                                            duration: 200
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+
+                // Add some bottom spacing for better scrolling
+                Item {
+                    Layout.preferredHeight: 20
+                }
+            }
+        }
+    }
+}

+ 182 - 2
dune-weaver-touch/qml/pages/ModernPlaylistPage.qml

@@ -773,7 +773,7 @@ Page {
                                                     font.bold: true
                                                     color: pauseGrid.currentSelection === "1 hour" ? "white" : Components.ThemeManager.textPrimary
                                                 }
-                                                
+
                                                 MouseArea {
                                                     anchors.fill: parent
                                                     onClicked: {
@@ -785,7 +785,187 @@ Page {
                                                     }
                                                 }
                                             }
-                                            
+                                        }
+
+                                        // Second row for longer hour options
+                                        RowLayout {
+                                            Layout.fillWidth: true
+                                            spacing: 8
+
+                                            // 2h button
+                                            Rectangle {
+                                                Layout.preferredWidth: 60
+                                                Layout.preferredHeight: 40
+                                                color: pauseGrid.currentSelection === "2 hour" ? Components.ThemeManager.selectedBackground : Components.ThemeManager.buttonBackground
+                                                border.color: pauseGrid.currentSelection === "2 hour" ? Components.ThemeManager.selectedBorder : Components.ThemeManager.buttonBorder
+                                                border.width: 2
+                                                radius: 8
+
+                                                Label {
+                                                    anchors.centerIn: parent
+                                                    text: "2h"
+                                                    font.pixelSize: 12
+                                                    font.bold: true
+                                                    color: pauseGrid.currentSelection === "2 hour" ? "white" : Components.ThemeManager.textPrimary
+                                                }
+
+                                                MouseArea {
+                                                    anchors.fill: parent
+                                                    onClicked: {
+                                                        if (backend) {
+                                                            backend.setPauseByOption("2 hour")
+                                                            pauseGrid.currentSelection = "2 hour"
+                                                            pauseTime = 7200
+                                                        }
+                                                    }
+                                                }
+                                            }
+
+                                            // 3h button
+                                            Rectangle {
+                                                Layout.preferredWidth: 60
+                                                Layout.preferredHeight: 40
+                                                color: pauseGrid.currentSelection === "3 hour" ? Components.ThemeManager.selectedBackground : Components.ThemeManager.buttonBackground
+                                                border.color: pauseGrid.currentSelection === "3 hour" ? Components.ThemeManager.selectedBorder : Components.ThemeManager.buttonBorder
+                                                border.width: 2
+                                                radius: 8
+
+                                                Label {
+                                                    anchors.centerIn: parent
+                                                    text: "3h"
+                                                    font.pixelSize: 12
+                                                    font.bold: true
+                                                    color: pauseGrid.currentSelection === "3 hour" ? "white" : Components.ThemeManager.textPrimary
+                                                }
+
+                                                MouseArea {
+                                                    anchors.fill: parent
+                                                    onClicked: {
+                                                        if (backend) {
+                                                            backend.setPauseByOption("3 hour")
+                                                            pauseGrid.currentSelection = "3 hour"
+                                                            pauseTime = 10800
+                                                        }
+                                                    }
+                                                }
+                                            }
+
+                                            // 4h button
+                                            Rectangle {
+                                                Layout.preferredWidth: 60
+                                                Layout.preferredHeight: 40
+                                                color: pauseGrid.currentSelection === "4 hour" ? Components.ThemeManager.selectedBackground : Components.ThemeManager.buttonBackground
+                                                border.color: pauseGrid.currentSelection === "4 hour" ? Components.ThemeManager.selectedBorder : Components.ThemeManager.buttonBorder
+                                                border.width: 2
+                                                radius: 8
+
+                                                Label {
+                                                    anchors.centerIn: parent
+                                                    text: "4h"
+                                                    font.pixelSize: 12
+                                                    font.bold: true
+                                                    color: pauseGrid.currentSelection === "4 hour" ? "white" : Components.ThemeManager.textPrimary
+                                                }
+
+                                                MouseArea {
+                                                    anchors.fill: parent
+                                                    onClicked: {
+                                                        if (backend) {
+                                                            backend.setPauseByOption("4 hour")
+                                                            pauseGrid.currentSelection = "4 hour"
+                                                            pauseTime = 14400
+                                                        }
+                                                    }
+                                                }
+                                            }
+
+                                            // 5h button
+                                            Rectangle {
+                                                Layout.preferredWidth: 60
+                                                Layout.preferredHeight: 40
+                                                color: pauseGrid.currentSelection === "5 hour" ? Components.ThemeManager.selectedBackground : Components.ThemeManager.buttonBackground
+                                                border.color: pauseGrid.currentSelection === "5 hour" ? Components.ThemeManager.selectedBorder : Components.ThemeManager.buttonBorder
+                                                border.width: 2
+                                                radius: 8
+
+                                                Label {
+                                                    anchors.centerIn: parent
+                                                    text: "5h"
+                                                    font.pixelSize: 12
+                                                    font.bold: true
+                                                    color: pauseGrid.currentSelection === "5 hour" ? "white" : Components.ThemeManager.textPrimary
+                                                }
+
+                                                MouseArea {
+                                                    anchors.fill: parent
+                                                    onClicked: {
+                                                        if (backend) {
+                                                            backend.setPauseByOption("5 hour")
+                                                            pauseGrid.currentSelection = "5 hour"
+                                                            pauseTime = 18000
+                                                        }
+                                                    }
+                                                }
+                                            }
+
+                                            // 6h button
+                                            Rectangle {
+                                                Layout.preferredWidth: 60
+                                                Layout.preferredHeight: 40
+                                                color: pauseGrid.currentSelection === "6 hour" ? Components.ThemeManager.selectedBackground : Components.ThemeManager.buttonBackground
+                                                border.color: pauseGrid.currentSelection === "6 hour" ? Components.ThemeManager.selectedBorder : Components.ThemeManager.buttonBorder
+                                                border.width: 2
+                                                radius: 8
+
+                                                Label {
+                                                    anchors.centerIn: parent
+                                                    text: "6h"
+                                                    font.pixelSize: 12
+                                                    font.bold: true
+                                                    color: pauseGrid.currentSelection === "6 hour" ? "white" : Components.ThemeManager.textPrimary
+                                                }
+
+                                                MouseArea {
+                                                    anchors.fill: parent
+                                                    onClicked: {
+                                                        if (backend) {
+                                                            backend.setPauseByOption("6 hour")
+                                                            pauseGrid.currentSelection = "6 hour"
+                                                            pauseTime = 21600
+                                                        }
+                                                    }
+                                                }
+                                            }
+
+                                            // 12h button
+                                            Rectangle {
+                                                Layout.preferredWidth: 60
+                                                Layout.preferredHeight: 40
+                                                color: pauseGrid.currentSelection === "12 hour" ? Components.ThemeManager.selectedBackground : Components.ThemeManager.buttonBackground
+                                                border.color: pauseGrid.currentSelection === "12 hour" ? Components.ThemeManager.selectedBorder : Components.ThemeManager.buttonBorder
+                                                border.width: 2
+                                                radius: 8
+
+                                                Label {
+                                                    anchors.centerIn: parent
+                                                    text: "12h"
+                                                    font.pixelSize: 12
+                                                    font.bold: true
+                                                    color: pauseGrid.currentSelection === "12 hour" ? "white" : Components.ThemeManager.textPrimary
+                                                }
+
+                                                MouseArea {
+                                                    anchors.fill: parent
+                                                    onClicked: {
+                                                        if (backend) {
+                                                            backend.setPauseByOption("12 hour")
+                                                            pauseGrid.currentSelection = "12 hour"
+                                                            pauseTime = 43200
+                                                        }
+                                                    }
+                                                }
+                                            }
+
                                             // Update selection when backend changes
                                             Connections {
                                                 target: backend

+ 2 - 1
dune-weaver-touch/requirements.txt

@@ -1,4 +1,5 @@
 PySide6>=6.5.0
 qasync>=0.27.0
 aiohttp>=3.9.0
-Pillow>=10.0.0
+Pillow>=10.0.0
+python-dotenv>=1.0.0