Procházet zdrojové kódy

feat(touch): add full-page pattern selector and enhance playlist management

Replace the pattern selector popup with a full-page navigation that mirrors
the Browse page layout. Patterns already in the playlist are highlighted with
a blue border and checkmark badge for easy identification.

Key changes:
- Add PatternSelectorPage.qml with grid layout and instant visual feedback
- Update ModernPlaylistPage to navigate to selector instead of popup
- Add playlist creation/deletion buttons and pattern removal functionality
- Enhance search with Enter-to-search pattern across pages
- Add raw pattern tracking for proper API calls

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris před 1 týdnem
rodič
revize
172295d0bf

+ 210 - 2
dune-weaver-touch/backend.py

@@ -31,10 +31,11 @@ class Backend(QObject):
         "Never": 0  # 0 means never timeout
         "Never": 0  # 0 means never timeout
     }
     }
     
     
-    # Predefined speed options 
+    # Predefined speed options
     SPEED_OPTIONS = {
     SPEED_OPTIONS = {
         "50": 50,
         "50": 50,
         "100": 100,
         "100": 100,
+        "150": 150,
         "200": 200,
         "200": 200,
         "300": 300,
         "300": 300,
         "500": 500
         "500": 500
@@ -72,6 +73,13 @@ class Backend(QObject):
     screenTimeoutChanged = Signal(int)  # New signal for timeout changes
     screenTimeoutChanged = Signal(int)  # New signal for timeout changes
     pauseBetweenPatternsChanged = Signal(int)  # New signal for pause changes
     pauseBetweenPatternsChanged = Signal(int)  # New signal for pause changes
     pausedChanged = Signal(bool)  # Signal when pause state changes
     pausedChanged = Signal(bool)  # Signal when pause state changes
+    patternsRefreshCompleted = Signal(bool, str)  # (success, message) for pattern refresh
+
+    # Playlist management signals
+    playlistCreated = Signal(bool, str)      # (success, message)
+    playlistDeleted = Signal(bool, str)      # (success, message)
+    patternAddedToPlaylist = Signal(bool, str)  # (success, message)
+    playlistModified = Signal(bool, str)     # (success, message)
 
 
     # Backend connection status signals
     # Backend connection status signals
     backendConnectionChanged = Signal(bool)  # True = backend reachable, False = unreachable
     backendConnectionChanged = Signal(bool)  # True = backend reachable, False = unreachable
@@ -1660,4 +1668,204 @@ class Backend(QObject):
                     self.errorOccurred.emit(f"Failed to set palette: {resp.status}")
                     self.errorOccurred.emit(f"Failed to set palette: {resp.status}")
         except Exception as e:
         except Exception as e:
             print(f"💥 Exception setting LED palette: {e}")
             print(f"💥 Exception setting LED palette: {e}")
-            self.errorOccurred.emit(str(e))
+            self.errorOccurred.emit(str(e))
+
+    # ==================== Pattern Refresh Methods ====================
+
+    @Slot()
+    def refreshPatterns(self):
+        """Refresh pattern cache - converts new WebPs to PNG and rescans patterns"""
+        print("🔄 Refreshing patterns...")
+        asyncio.create_task(self._refresh_patterns())
+
+    async def _refresh_patterns(self):
+        """Async implementation of pattern refresh"""
+        try:
+            from png_cache_manager import PngCacheManager
+            cache_manager = PngCacheManager()
+            success = await cache_manager.ensure_png_cache_available()
+
+            message = "Patterns refreshed" if success else "Refreshed with warnings"
+            print(f"✅ Pattern refresh completed: {message}")
+            self.patternsRefreshCompleted.emit(True, message)
+        except Exception as e:
+            print(f"❌ Pattern refresh failed: {e}")
+            self.patternsRefreshCompleted.emit(False, str(e))
+
+    # ==================== System Control Methods ====================
+
+    @Slot()
+    def restartBackend(self):
+        """Restart the dune-weaver backend via API"""
+        print("🔄 Requesting backend restart via API...")
+        asyncio.create_task(self._restart_backend())
+
+    async def _restart_backend(self):
+        """Async implementation of backend restart"""
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+
+        try:
+            async with self.session.post(f"{self.base_url}/api/system/restart") as resp:
+                if resp.status == 200:
+                    print("✅ Backend restart initiated via API")
+                else:
+                    response_text = await resp.text()
+                    print(f"❌ Failed to restart backend: {resp.status} - {response_text}")
+                    self.errorOccurred.emit(f"Failed to restart: {response_text}")
+        except Exception as e:
+            print(f"💥 Exception restarting backend: {e}")
+            self.errorOccurred.emit(str(e))
+
+    @Slot()
+    def shutdownPi(self):
+        """Shutdown the Raspberry Pi via API"""
+        print("⏻ Requesting Pi shutdown via API...")
+        asyncio.create_task(self._shutdown_pi())
+
+    async def _shutdown_pi(self):
+        """Async implementation of Pi shutdown"""
+        if not self.session:
+            self.errorOccurred.emit("Backend not ready")
+            return
+
+        try:
+            async with self.session.post(f"{self.base_url}/api/system/shutdown") as resp:
+                if resp.status == 200:
+                    print("✅ Shutdown initiated via API")
+                else:
+                    response_text = await resp.text()
+                    print(f"❌ Failed to shutdown: {resp.status} - {response_text}")
+                    self.errorOccurred.emit(f"Failed to shutdown: {response_text}")
+        except Exception as e:
+            print(f"💥 Exception during shutdown: {e}")
+            self.errorOccurred.emit(str(e))
+
+    # ==================== Playlist Management Methods ====================
+
+    @Slot(str)
+    def createPlaylist(self, playlistName):
+        """Create a new empty playlist"""
+        print(f"📋 Creating playlist: {playlistName}")
+        asyncio.create_task(self._create_playlist(playlistName))
+
+    async def _create_playlist(self, playlistName):
+        """Async implementation of playlist creation"""
+        if not self.session:
+            self.playlistCreated.emit(False, "Backend not ready")
+            return
+
+        try:
+            async with self.session.post(
+                f"{self.base_url}/create_playlist",
+                json={"playlist_name": playlistName, "files": []}
+            ) as resp:
+                if resp.status == 200:
+                    print(f"✅ Playlist created: {playlistName}")
+                    self.playlistCreated.emit(True, f"Created: {playlistName}")
+                else:
+                    response_text = await resp.text()
+                    print(f"❌ Failed to create playlist: {resp.status} - {response_text}")
+                    self.playlistCreated.emit(False, f"Failed: {response_text}")
+        except Exception as e:
+            print(f"💥 Exception creating playlist: {e}")
+            self.playlistCreated.emit(False, str(e))
+
+    @Slot(str)
+    def deletePlaylist(self, playlistName):
+        """Delete a playlist"""
+        print(f"🗑️ Deleting playlist: {playlistName}")
+        asyncio.create_task(self._delete_playlist(playlistName))
+
+    async def _delete_playlist(self, playlistName):
+        """Async implementation of playlist deletion"""
+        if not self.session:
+            self.playlistDeleted.emit(False, "Backend not ready")
+            return
+
+        try:
+            async with self.session.request(
+                "DELETE",
+                f"{self.base_url}/delete_playlist",
+                json={"playlist_name": playlistName}
+            ) as resp:
+                if resp.status == 200:
+                    print(f"✅ Playlist deleted: {playlistName}")
+                    self.playlistDeleted.emit(True, f"Deleted: {playlistName}")
+                else:
+                    response_text = await resp.text()
+                    print(f"❌ Failed to delete playlist: {resp.status} - {response_text}")
+                    self.playlistDeleted.emit(False, f"Failed: {response_text}")
+        except Exception as e:
+            print(f"💥 Exception deleting playlist: {e}")
+            self.playlistDeleted.emit(False, str(e))
+
+    @Slot(str, str)
+    def addPatternToPlaylist(self, playlistName, patternPath):
+        """Add a pattern to an existing playlist"""
+        print(f"➕ Adding pattern to playlist: {patternPath} -> {playlistName}")
+        asyncio.create_task(self._add_pattern_to_playlist(playlistName, patternPath))
+
+    async def _add_pattern_to_playlist(self, playlistName, patternPath):
+        """Async implementation of adding pattern to playlist"""
+        if not self.session:
+            self.patternAddedToPlaylist.emit(False, "Backend not ready")
+            return
+
+        try:
+            async with self.session.post(
+                f"{self.base_url}/add_to_playlist",
+                json={"playlist_name": playlistName, "pattern": patternPath}
+            ) as resp:
+                if resp.status == 200:
+                    print(f"✅ Pattern added to {playlistName}")
+                    self.patternAddedToPlaylist.emit(True, f"Added to {playlistName}")
+                else:
+                    response_text = await resp.text()
+                    print(f"❌ Failed to add pattern: {resp.status} - {response_text}")
+                    self.patternAddedToPlaylist.emit(False, f"Failed: {response_text}")
+        except Exception as e:
+            print(f"💥 Exception adding pattern: {e}")
+            self.patternAddedToPlaylist.emit(False, str(e))
+
+    @Slot(str, list)
+    def updatePlaylistPatterns(self, playlistName, patterns):
+        """Update a playlist with a new list of patterns (used for removing patterns)"""
+        print(f"📝 Updating playlist patterns: {playlistName} -> {len(patterns)} patterns")
+        asyncio.create_task(self._update_playlist_patterns(playlistName, patterns))
+
+    async def _update_playlist_patterns(self, playlistName, patterns):
+        """Async implementation of playlist pattern update"""
+        if not self.session:
+            self.playlistModified.emit(False, "Backend not ready")
+            return
+
+        try:
+            async with self.session.post(
+                f"{self.base_url}/modify_playlist",
+                json={"playlist_name": playlistName, "files": patterns}
+            ) as resp:
+                if resp.status == 200:
+                    print(f"✅ Playlist updated: {playlistName}")
+                    self.playlistModified.emit(True, f"Updated: {playlistName}")
+                else:
+                    response_text = await resp.text()
+                    print(f"❌ Failed to update playlist: {resp.status} - {response_text}")
+                    self.playlistModified.emit(False, f"Failed: {response_text}")
+        except Exception as e:
+            print(f"💥 Exception updating playlist: {e}")
+            self.playlistModified.emit(False, str(e))
+
+    @Slot(result=list)
+    def getPlaylistNames(self):
+        """Get list of all playlist names (synchronous, reads from local file)"""
+        try:
+            playlists_file = Path("../playlists.json")
+            if playlists_file.exists():
+                with open(playlists_file, 'r') as f:
+                    data = json.load(f)
+                    return sorted(list(data.keys()))
+        except Exception as e:
+            print(f"💥 Error reading playlists: {e}")
+        return []

+ 17 - 1
dune-weaver-touch/models/playlist_model.py

@@ -67,7 +67,7 @@ class PlaylistModel(QAbstractListModel):
     
     
     @Slot(str, result=list)
     @Slot(str, result=list)
     def getPatternsForPlaylist(self, playlistName):
     def getPatternsForPlaylist(self, playlistName):
-        """Get the list of patterns for a given playlist"""
+        """Get the list of patterns for a given playlist (cleaned for display)"""
         if hasattr(self, '_playlist_data') and playlistName in self._playlist_data:
         if hasattr(self, '_playlist_data') and playlistName in self._playlist_data:
             patterns = self._playlist_data[playlistName]
             patterns = self._playlist_data[playlistName]
             if isinstance(patterns, list):
             if isinstance(patterns, list):
@@ -82,4 +82,20 @@ class PlaylistModel(QAbstractListModel):
                         clean_name = clean_name[:-4]
                         clean_name = clean_name[:-4]
                     cleaned_patterns.append(clean_name)
                     cleaned_patterns.append(clean_name)
                 return cleaned_patterns
                 return cleaned_patterns
+        return []
+
+    @Slot(str, result=list)
+    def getRawPatternsForPlaylist(self, playlistName):
+        """Get the raw list of patterns for a playlist (with full paths, for API calls)"""
+        if hasattr(self, '_playlist_data') and playlistName in self._playlist_data:
+            patterns = self._playlist_data[playlistName]
+            if isinstance(patterns, list):
+                return patterns
+        return []
+
+    @Slot(result=list)
+    def getAllPlaylistNames(self):
+        """Get a list of all playlist names"""
+        if hasattr(self, '_playlist_data'):
+            return sorted(list(self._playlist_data.keys()))
         return []
         return []

+ 3 - 3
dune-weaver-touch/qml/pages/ExecutionPage.qml

@@ -429,10 +429,10 @@ Page {
                                     
                                     
                                     // Speed buttons
                                     // Speed buttons
                                     Repeater {
                                     Repeater {
-                                        model: ["100", "150", "200", "300", "500"]
-                                        
+                                        model: ["50", "100", "150", "200", "300", "500"]
+
                                         Rectangle {
                                         Rectangle {
-                                            width: (speedControlRow.width - 32) / 5  // Distribute evenly with spacing
+                                            width: (speedControlRow.width - 40) / 6  // Distribute evenly with spacing
                                             height: 50
                                             height: 50
                                             color: speedControlRow.currentSelection === modelData ? Components.ThemeManager.selectedBackground : Components.ThemeManager.buttonBackground
                                             color: speedControlRow.currentSelection === modelData ? Components.ThemeManager.selectedBackground : Components.ThemeManager.buttonBackground
                                             border.color: speedControlRow.currentSelection === modelData ? Components.ThemeManager.selectedBorder : Components.ThemeManager.buttonBorder
                                             border.color: speedControlRow.currentSelection === modelData ? Components.ThemeManager.selectedBorder : Components.ThemeManager.buttonBorder

+ 65 - 5
dune-weaver-touch/qml/pages/ModernPatternListPage.qml

@@ -11,6 +11,28 @@ Page {
     property var backend
     property var backend
     property var stackView
     property var stackView
     property bool searchExpanded: false
     property bool searchExpanded: false
+    property bool isRefreshing: false
+    property int patternCount: patternModel ? patternModel.rowCount() : 0
+
+    // Handle pattern refresh completion from backend
+    Connections {
+        target: backend
+        function onPatternsRefreshCompleted(success, message) {
+            console.log("🔄 Pattern refresh completed:", success, message)
+            if (patternModel) {
+                patternModel.refresh()
+            }
+            isRefreshing = false
+        }
+    }
+
+    // Update pattern count when model resets (rowCount() is not reactive)
+    Connections {
+        target: patternModel
+        function onModelReset() {
+            patternCount = patternModel.rowCount()
+        }
+    }
 
 
     Rectangle {
     Rectangle {
         anchors.fill: parent
         anchors.fill: parent
@@ -56,14 +78,52 @@ Page {
 
 
                 // Pattern count
                 // Pattern count
                 Label {
                 Label {
-                    text: patternModel.rowCount() + " patterns"
+                    text: patternCount + " patterns"
                     font.pixelSize: 12
                     font.pixelSize: 12
                     color: Components.ThemeManager.textTertiary
                     color: Components.ThemeManager.textTertiary
                     visible: !searchExpanded
                     visible: !searchExpanded
                 }
                 }
-                
-                Item { 
-                    Layout.fillWidth: true 
+
+                // Refresh button
+                Rectangle {
+                    Layout.preferredWidth: 32
+                    Layout.preferredHeight: 32
+                    radius: 16
+                    color: refreshMouseArea.pressed ? Components.ThemeManager.buttonBackgroundHover :
+                           (refreshMouseArea.containsMouse ? Components.ThemeManager.cardColor : "transparent")
+                    visible: !searchExpanded
+
+                    Text {
+                        id: refreshIcon
+                        anchors.centerIn: parent
+                        text: "↻"
+                        font.pixelSize: 16
+                        color: isRefreshing ? Components.ThemeManager.accentBlue : Components.ThemeManager.textSecondary
+
+                        SequentialAnimation on opacity {
+                            running: isRefreshing
+                            loops: Animation.Infinite
+                            NumberAnimation { to: 0.4; duration: 500 }
+                            NumberAnimation { to: 1.0; duration: 500 }
+                        }
+                    }
+
+                    MouseArea {
+                        id: refreshMouseArea
+                        anchors.fill: parent
+                        hoverEnabled: true
+                        enabled: !isRefreshing
+                        onClicked: {
+                            if (backend) {
+                                isRefreshing = true
+                                backend.refreshPatterns()
+                            }
+                        }
+                    }
+                }
+
+                Item {
+                    Layout.fillWidth: true
                     visible: !searchExpanded
                     visible: !searchExpanded
                 }
                 }
                 
                 
@@ -254,7 +314,7 @@ Page {
         Item {
         Item {
             Layout.fillWidth: true
             Layout.fillWidth: true
             Layout.fillHeight: true
             Layout.fillHeight: true
-            visible: patternModel.rowCount() === 0 && searchField.text !== ""
+            visible: patternCount === 0 && searchField.text !== ""
 
 
             Column {
             Column {
                 anchors.centerIn: parent
                 anchors.centerIn: parent

+ 369 - 16
dune-weaver-touch/qml/pages/ModernPlaylistPage.qml

@@ -17,6 +17,7 @@ Page {
     property string selectedPlaylist: ""
     property string selectedPlaylist: ""
     property var selectedPlaylistData: null
     property var selectedPlaylistData: null
     property var currentPlaylistPatterns: []
     property var currentPlaylistPatterns: []
+    property var currentPlaylistRawPatterns: []  // Raw patterns with full paths for API calls
     
     
     // Playlist execution settings
     // Playlist execution settings
     property real pauseTime: backend ? backend.pauseBetweenPatterns : 0
     property real pauseTime: backend ? backend.pauseBetweenPatterns : 0
@@ -32,9 +33,20 @@ Page {
     onSelectedPlaylistChanged: {
     onSelectedPlaylistChanged: {
         if (selectedPlaylist) {
         if (selectedPlaylist) {
             currentPlaylistPatterns = playlistModel.getPatternsForPlaylist(selectedPlaylist)
             currentPlaylistPatterns = playlistModel.getPatternsForPlaylist(selectedPlaylist)
+            currentPlaylistRawPatterns = playlistModel.getRawPatternsForPlaylist(selectedPlaylist)
             console.log("Loaded patterns for", selectedPlaylist + ":", currentPlaylistPatterns)
             console.log("Loaded patterns for", selectedPlaylist + ":", currentPlaylistPatterns)
         } else {
         } else {
             currentPlaylistPatterns = []
             currentPlaylistPatterns = []
+            currentPlaylistRawPatterns = []
+        }
+    }
+
+    // Function to remove a pattern from the current playlist
+    function removePatternAtIndex(index) {
+        if (index >= 0 && index < currentPlaylistRawPatterns.length && backend) {
+            var updatedPatterns = currentPlaylistRawPatterns.slice()  // Create a copy
+            updatedPatterns.splice(index, 1)  // Remove the pattern at index
+            backend.updatePlaylistPatterns(selectedPlaylist, updatedPatterns)
         }
         }
     }
     }
     
     
@@ -110,9 +122,24 @@ Page {
                         font.pixelSize: 12
                         font.pixelSize: 12
                         color: Components.ThemeManager.textTertiary
                         color: Components.ThemeManager.textTertiary
                     }
                     }
-                    
-                    Item { 
-                        Layout.fillWidth: true 
+
+                    Item {
+                        Layout.fillWidth: true
+                    }
+
+                    // Create new playlist button
+                    Text {
+                        text: "+"
+                        font.pixelSize: 32
+                        font.bold: true
+                        color: createPlaylistMouseArea.pressed ? "#1e40af" : "#2563eb"
+
+                        MouseArea {
+                            id: createPlaylistMouseArea
+                            anchors.fill: parent
+                            anchors.margins: -8  // Increase touch area
+                            onClicked: createPlaylistDialog.open()
+                        }
                     }
                     }
                 }
                 }
             }
             }
@@ -306,6 +333,20 @@ Page {
                         font.pixelSize: 12
                         font.pixelSize: 12
                         color: Components.ThemeManager.textTertiary
                         color: Components.ThemeManager.textTertiary
                     }
                     }
+
+                    // Delete playlist button
+                    Text {
+                        text: "🗑"
+                        font.pixelSize: 20
+                        color: deletePlaylistMouseArea.pressed ? "#dc2626" : Components.ThemeManager.textSecondary
+
+                        MouseArea {
+                            id: deletePlaylistMouseArea
+                            anchors.fill: parent
+                            anchors.margins: -8
+                            onClicked: deletePlaylistDialog.open()
+                        }
+                    }
                 }
                 }
             }
             }
             
             
@@ -328,14 +369,43 @@ Page {
                             anchors.fill: parent
                             anchors.fill: parent
                             anchors.margins: 15
                             anchors.margins: 15
                             spacing: 10
                             spacing: 10
-                            
-                            Label {
-                                text: "Patterns"
-                                font.pixelSize: 14
-                                font.bold: true
-                                color: Components.ThemeManager.textPrimary
+
+                            RowLayout {
+                                Layout.fillWidth: true
+                                spacing: 10
+
+                                Label {
+                                    text: "Patterns"
+                                    font.pixelSize: 14
+                                    font.bold: true
+                                    color: Components.ThemeManager.textPrimary
+                                    Layout.fillWidth: true
+                                }
+
+                                // Add pattern button
+                                Text {
+                                    text: "+"
+                                    font.pixelSize: 24
+                                    font.bold: true
+                                    color: addPatternMouseArea.pressed ? "#1e40af" : "#2563eb"
+
+                                    MouseArea {
+                                        id: addPatternMouseArea
+                                        anchors.fill: parent
+                                        anchors.margins: -8
+                                        onClicked: {
+                                            // Navigate to full-page pattern selector
+                                            stackView.push("PatternSelectorPage.qml", {
+                                                backend: backend,
+                                                stackView: stackView,
+                                                playlistName: selectedPlaylist,
+                                                existingPatterns: currentPlaylistRawPatterns
+                                            })
+                                        }
+                                    }
+                                }
                             }
                             }
-                            
+
                             ScrollView {
                             ScrollView {
                                 Layout.fillWidth: true
                                 Layout.fillWidth: true
                                 Layout.fillHeight: true
                                 Layout.fillHeight: true
@@ -349,22 +419,25 @@ Page {
                                     
                                     
                                     delegate: Rectangle {
                                     delegate: Rectangle {
                                         width: patternListView.width
                                         width: patternListView.width
-                                        height: 35
+                                        height: 40
                                         color: index % 2 === 0 ? Components.ThemeManager.cardColor : Components.ThemeManager.surfaceColor
                                         color: index % 2 === 0 ? Components.ThemeManager.cardColor : Components.ThemeManager.surfaceColor
                                         radius: 6
                                         radius: 6
                                         border.color: Components.ThemeManager.borderColor
                                         border.color: Components.ThemeManager.borderColor
                                         border.width: 1
                                         border.width: 1
-                                        
+
                                         RowLayout {
                                         RowLayout {
                                             anchors.fill: parent
                                             anchors.fill: parent
-                                            anchors.margins: 10
-                                            spacing: 8
-                                            
+                                            anchors.leftMargin: 10
+                                            anchors.rightMargin: 5
+                                            anchors.topMargin: 4
+                                            anchors.bottomMargin: 4
+                                            spacing: 6
+
                                             Label {
                                             Label {
                                                 text: (index + 1) + "."
                                                 text: (index + 1) + "."
                                                 font.pixelSize: 11
                                                 font.pixelSize: 11
                                                 color: Components.ThemeManager.textSecondary
                                                 color: Components.ThemeManager.textSecondary
-                                                Layout.preferredWidth: 25
+                                                Layout.preferredWidth: 22
                                             }
                                             }
 
 
                                             Label {
                                             Label {
@@ -374,6 +447,23 @@ Page {
                                                 Layout.fillWidth: true
                                                 Layout.fillWidth: true
                                                 elide: Text.ElideRight
                                                 elide: Text.ElideRight
                                             }
                                             }
+
+                                            // Remove pattern button - aligned right
+                                            Text {
+                                                text: "✕"
+                                                font.pixelSize: 16
+                                                color: removePatternArea.pressed ? "#ef4444" : Components.ThemeManager.textTertiary
+                                                Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+
+                                                MouseArea {
+                                                    id: removePatternArea
+                                                    anchors.fill: parent
+                                                    anchors.margins: -8  // Increase touch area
+                                                    onClicked: {
+                                                        removePatternAtIndex(index)
+                                                    }
+                                                }
+                                            }
                                         }
                                         }
                                     }
                                     }
                                 }
                                 }
@@ -1086,4 +1176,267 @@ Page {
             }
             }
         }
         }
     }
     }
+
+    // ==================== Dialogs ====================
+
+    // Create Playlist Dialog
+    Popup {
+        id: createPlaylistDialog
+        modal: true
+        x: (parent.width - width) / 2
+        y: (parent.height - height) / 2
+        width: 320
+        height: 200
+        closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+        background: Rectangle {
+            color: Components.ThemeManager.surfaceColor
+            radius: 16
+            border.color: Components.ThemeManager.borderColor
+            border.width: 1
+        }
+
+        contentItem: ColumnLayout {
+            anchors.fill: parent
+            anchors.margins: 20
+            spacing: 15
+
+            Label {
+                text: "Create New Playlist"
+                font.pixelSize: 18
+                font.bold: true
+                color: Components.ThemeManager.textPrimary
+                Layout.alignment: Qt.AlignHCenter
+            }
+
+            TextField {
+                id: newPlaylistNameField
+                Layout.fillWidth: true
+                Layout.preferredHeight: 45
+                placeholderText: "Enter playlist name..."
+                placeholderTextColor: Components.ThemeManager.textTertiary
+                font.pixelSize: 14
+                color: Components.ThemeManager.textPrimary
+
+                background: Rectangle {
+                    color: Components.ThemeManager.backgroundColor
+                    radius: 8
+                    border.color: newPlaylistNameField.activeFocus ? "#2563eb" : Components.ThemeManager.borderColor
+                    border.width: 1
+                }
+
+                onAccepted: {
+                    if (text.trim().length > 0 && backend) {
+                        backend.createPlaylist(text.trim())
+                    }
+                }
+            }
+
+            RowLayout {
+                Layout.fillWidth: true
+                spacing: 10
+
+                // Cancel button
+                Rectangle {
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: 45
+                    radius: 8
+                    color: cancelCreateArea.pressed ? Components.ThemeManager.buttonBackgroundHover : Components.ThemeManager.cardColor
+                    border.color: Components.ThemeManager.borderColor
+                    border.width: 1
+
+                    Text {
+                        anchors.centerIn: parent
+                        text: "Cancel"
+                        color: Components.ThemeManager.textPrimary
+                        font.pixelSize: 14
+                    }
+
+                    MouseArea {
+                        id: cancelCreateArea
+                        anchors.fill: parent
+                        onClicked: {
+                            newPlaylistNameField.text = ""
+                            createPlaylistDialog.close()
+                        }
+                    }
+                }
+
+                // Create button
+                Rectangle {
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: 45
+                    radius: 8
+                    color: createArea.pressed ? "#1e40af" : "#2563eb"
+                    opacity: newPlaylistNameField.text.trim().length > 0 ? 1.0 : 0.5
+
+                    Text {
+                        anchors.centerIn: parent
+                        text: "Create"
+                        color: "white"
+                        font.pixelSize: 14
+                        font.bold: true
+                    }
+
+                    MouseArea {
+                        id: createArea
+                        anchors.fill: parent
+                        enabled: newPlaylistNameField.text.trim().length > 0
+                        onClicked: {
+                            if (backend) {
+                                backend.createPlaylist(newPlaylistNameField.text.trim())
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        onOpened: {
+            newPlaylistNameField.text = ""
+            newPlaylistNameField.forceActiveFocus()
+        }
+    }
+
+    // Delete Playlist Confirmation Dialog
+    Popup {
+        id: deletePlaylistDialog
+        modal: true
+        x: (parent.width - width) / 2
+        y: (parent.height - height) / 2
+        width: 320
+        height: 180
+        closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+        background: Rectangle {
+            color: Components.ThemeManager.surfaceColor
+            radius: 16
+            border.color: Components.ThemeManager.borderColor
+            border.width: 1
+        }
+
+        contentItem: ColumnLayout {
+            anchors.fill: parent
+            anchors.margins: 20
+            spacing: 15
+
+            Label {
+                text: "Delete Playlist?"
+                font.pixelSize: 18
+                font.bold: true
+                color: Components.ThemeManager.textPrimary
+                Layout.alignment: Qt.AlignHCenter
+            }
+
+            Label {
+                text: "Are you sure you want to delete\n\"" + selectedPlaylist + "\"?"
+                font.pixelSize: 14
+                color: Components.ThemeManager.textSecondary
+                horizontalAlignment: Text.AlignHCenter
+                Layout.alignment: Qt.AlignHCenter
+            }
+
+            RowLayout {
+                Layout.fillWidth: true
+                spacing: 10
+
+                // Cancel button
+                Rectangle {
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: 45
+                    radius: 8
+                    color: cancelDeleteArea.pressed ? Components.ThemeManager.buttonBackgroundHover : Components.ThemeManager.cardColor
+                    border.color: Components.ThemeManager.borderColor
+                    border.width: 1
+
+                    Text {
+                        anchors.centerIn: parent
+                        text: "Cancel"
+                        color: Components.ThemeManager.textPrimary
+                        font.pixelSize: 14
+                    }
+
+                    MouseArea {
+                        id: cancelDeleteArea
+                        anchors.fill: parent
+                        onClicked: deletePlaylistDialog.close()
+                    }
+                }
+
+                // Delete button
+                Rectangle {
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: 45
+                    radius: 8
+                    color: confirmDeleteArea.pressed ? "#991b1b" : "#dc2626"
+
+                    Text {
+                        anchors.centerIn: parent
+                        text: "Delete"
+                        color: "white"
+                        font.pixelSize: 14
+                        font.bold: true
+                    }
+
+                    MouseArea {
+                        id: confirmDeleteArea
+                        anchors.fill: parent
+                        onClicked: {
+                            if (backend && selectedPlaylist) {
+                                backend.deletePlaylist(selectedPlaylist)
+                            }
+                            deletePlaylistDialog.close()
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    // ==================== Backend Signal Handlers ====================
+
+    Connections {
+        target: backend
+
+        function onPlaylistCreated(success, message) {
+            console.log("📋 Playlist created:", success, message)
+            if (success) {
+                playlistModel.refresh()
+            }
+            newPlaylistNameField.text = ""
+            createPlaylistDialog.close()
+        }
+
+        function onPlaylistDeleted(success, message) {
+            console.log("🗑️ Playlist deleted:", success, message)
+            if (success) {
+                playlistModel.refresh()
+                showPlaylistList()  // Navigate back to list
+            }
+        }
+
+        function onPatternAddedToPlaylist(success, message) {
+            console.log("➕ Pattern added to playlist:", success, message)
+            if (success) {
+                playlistModel.refresh()
+                // Refresh current playlist patterns if we're viewing one
+                if (selectedPlaylist) {
+                    currentPlaylistPatterns = playlistModel.getPatternsForPlaylist(selectedPlaylist)
+                    currentPlaylistRawPatterns = playlistModel.getRawPatternsForPlaylist(selectedPlaylist)
+                }
+            }
+        }
+
+        function onPlaylistModified(success, message) {
+            console.log("📝 Playlist modified:", success, message)
+            if (success) {
+                playlistModel.refresh()
+                // Refresh current playlist patterns
+                if (selectedPlaylist) {
+                    currentPlaylistPatterns = playlistModel.getPatternsForPlaylist(selectedPlaylist)
+                    currentPlaylistRawPatterns = playlistModel.getRawPatternsForPlaylist(selectedPlaylist)
+                }
+            }
+        }
+    }
 }
 }

+ 212 - 4
dune-weaver-touch/qml/pages/PatternDetailPage.qml

@@ -1,6 +1,7 @@
 import QtQuick 2.15
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.15
 import QtQuick.Layouts 1.15
+import DuneWeaver 1.0
 import "../components"
 import "../components"
 import "../components" as Components
 import "../components" as Components
 
 
@@ -10,6 +11,12 @@ Page {
     property string patternPath: ""
     property string patternPath: ""
     property string patternPreview: ""
     property string patternPreview: ""
     property var backend: null
     property var backend: null
+    property bool showAddedFeedback: false
+
+    // Playlist model for selecting which playlist to add to
+    PlaylistModel {
+        id: playlistModel
+    }
 
 
     Rectangle {
     Rectangle {
         anchors.fill: parent
         anchors.fill: parent
@@ -141,7 +148,7 @@ Page {
                         height: 50
                         height: 50
                         radius: 8
                         radius: 8
                         color: playMouseArea.pressed ? "#1e40af" : (backend ? "#2563eb" : "#9ca3af")
                         color: playMouseArea.pressed ? "#1e40af" : (backend ? "#2563eb" : "#9ca3af")
-                        
+
                         Text {
                         Text {
                             anchors.centerIn: parent
                             anchors.centerIn: parent
                             text: "▶ Play Pattern"
                             text: "▶ Play Pattern"
@@ -149,7 +156,7 @@ Page {
                             font.pixelSize: 16
                             font.pixelSize: 16
                             font.bold: true
                             font.bold: true
                         }
                         }
-                        
+
                         MouseArea {
                         MouseArea {
                             id: playMouseArea
                             id: playMouseArea
                             anchors.fill: parent
                             anchors.fill: parent
@@ -160,13 +167,49 @@ Page {
                                     if (centerRadio.checked) preExecution = "clear_center"
                                     if (centerRadio.checked) preExecution = "clear_center"
                                     else if (perimeterRadio.checked) preExecution = "clear_perimeter"
                                     else if (perimeterRadio.checked) preExecution = "clear_perimeter"
                                     else if (noneRadio.checked) preExecution = "none"
                                     else if (noneRadio.checked) preExecution = "none"
-                                    
+
                                     backend.executePattern(patternName, preExecution)
                                     backend.executePattern(patternName, preExecution)
                                 }
                                 }
                             }
                             }
                         }
                         }
                     }
                     }
-                    
+
+                    // Add to Playlist Button
+                    Rectangle {
+                        width: parent.width
+                        height: 45
+                        radius: 8
+                        color: addToPlaylistArea.pressed ? "#065f46" : "#059669"
+
+                        Row {
+                            anchors.centerIn: parent
+                            spacing: 8
+
+                            Text {
+                                text: showAddedFeedback ? "✓" : "♪"
+                                font.pixelSize: 16
+                                color: "white"
+                            }
+
+                            Text {
+                                text: showAddedFeedback ? "Added!" : "Add to Playlist"
+                                color: "white"
+                                font.pixelSize: 14
+                                font.bold: true
+                            }
+                        }
+
+                        MouseArea {
+                            id: addToPlaylistArea
+                            anchors.fill: parent
+                            enabled: backend !== null && !showAddedFeedback
+                            onClicked: {
+                                playlistModel.refresh()  // Refresh playlist list
+                                playlistSelectorPopup.open()
+                            }
+                        }
+                    }
+
                     // Pre-Execution Options
                     // Pre-Execution Options
                     Rectangle {
                     Rectangle {
                         width: parent.width
                         width: parent.width
@@ -294,4 +337,169 @@ Page {
             }
             }
         }
         }
     }
     }
+
+    // ==================== Playlist Selector Popup ====================
+
+    Popup {
+        id: playlistSelectorPopup
+        modal: true
+        x: (parent.width - width) / 2
+        y: (parent.height - height) / 2
+        width: 320
+        height: Math.min(400, 120 + playlistModel.rowCount() * 50)
+        closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+        background: Rectangle {
+            color: Components.ThemeManager.surfaceColor
+            radius: 16
+            border.color: Components.ThemeManager.borderColor
+            border.width: 1
+        }
+
+        contentItem: ColumnLayout {
+            anchors.fill: parent
+            anchors.margins: 15
+            spacing: 10
+
+            Label {
+                text: "Add to Playlist"
+                font.pixelSize: 18
+                font.bold: true
+                color: Components.ThemeManager.textPrimary
+                Layout.alignment: Qt.AlignHCenter
+            }
+
+            // Playlist list
+            ListView {
+                Layout.fillWidth: true
+                Layout.fillHeight: true
+                clip: true
+                model: playlistModel
+                spacing: 6
+
+                delegate: Rectangle {
+                    width: ListView.view.width
+                    height: 45
+                    radius: 8
+                    color: playlistItemArea.pressed ? Components.ThemeManager.selectedBackground : Components.ThemeManager.cardColor
+                    border.color: Components.ThemeManager.borderColor
+                    border.width: 1
+
+                    RowLayout {
+                        anchors.fill: parent
+                        anchors.margins: 12
+                        spacing: 10
+
+                        Text {
+                            text: "♪"
+                            font.pixelSize: 16
+                            color: "#2196F3"
+                        }
+
+                        Label {
+                            text: model.name
+                            font.pixelSize: 14
+                            color: Components.ThemeManager.textPrimary
+                            Layout.fillWidth: true
+                            elide: Text.ElideRight
+                        }
+
+                        Label {
+                            text: model.itemCount + " patterns"
+                            font.pixelSize: 11
+                            color: Components.ThemeManager.textTertiary
+                        }
+                    }
+
+                    MouseArea {
+                        id: playlistItemArea
+                        anchors.fill: parent
+                        onClicked: {
+                            if (backend) {
+                                backend.addPatternToPlaylist(model.name, patternName)
+                            }
+                            playlistSelectorPopup.close()
+                        }
+                    }
+                }
+            }
+
+            // Empty state
+            Column {
+                Layout.fillWidth: true
+                Layout.fillHeight: true
+                spacing: 10
+                visible: playlistModel.rowCount() === 0
+
+                Item { Layout.fillHeight: true }
+
+                Text {
+                    text: "♪"
+                    font.pixelSize: 32
+                    color: Components.ThemeManager.placeholderText
+                    anchors.horizontalCenter: parent.horizontalCenter
+                }
+
+                Label {
+                    text: "No playlists yet"
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    color: Components.ThemeManager.textSecondary
+                    font.pixelSize: 14
+                }
+
+                Label {
+                    text: "Create a playlist first"
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    color: Components.ThemeManager.textTertiary
+                    font.pixelSize: 12
+                }
+
+                Item { Layout.fillHeight: true }
+            }
+
+            // Cancel button
+            Rectangle {
+                Layout.fillWidth: true
+                Layout.preferredHeight: 40
+                radius: 8
+                color: cancelArea.pressed ? Components.ThemeManager.buttonBackgroundHover : Components.ThemeManager.cardColor
+                border.color: Components.ThemeManager.borderColor
+                border.width: 1
+
+                Text {
+                    anchors.centerIn: parent
+                    text: "Cancel"
+                    color: Components.ThemeManager.textPrimary
+                    font.pixelSize: 14
+                }
+
+                MouseArea {
+                    id: cancelArea
+                    anchors.fill: parent
+                    onClicked: playlistSelectorPopup.close()
+                }
+            }
+        }
+    }
+
+    // ==================== Backend Signal Handlers ====================
+
+    Connections {
+        target: backend
+
+        function onPatternAddedToPlaylist(success, message) {
+            console.log("➕ Pattern added to playlist:", success, message)
+            if (success) {
+                // Show feedback
+                showAddedFeedback = true
+                feedbackTimer.start()
+            }
+        }
+    }
+
+    Timer {
+        id: feedbackTimer
+        interval: 2000  // Show "Added!" for 2 seconds
+        onTriggered: showAddedFeedback = false
+    }
 }
 }

+ 393 - 0
dune-weaver-touch/qml/pages/PatternSelectorPage.qml

@@ -0,0 +1,393 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import DuneWeaver 1.0
+import "../components"
+import "../components" as Components
+
+Page {
+    id: page
+
+    property var backend: null
+    property var stackView: null
+    property string playlistName: ""
+    property var existingPatterns: []  // Raw pattern names already in playlist
+
+    // Track patterns added in this session for immediate visual feedback
+    property var sessionAddedPatterns: []
+
+    // Local pattern model for this page
+    PatternModel {
+        id: patternModel
+    }
+
+    // Search state
+    property bool searchExpanded: false
+    property int patternCount: patternModel ? patternModel.rowCount() : 0
+
+    // Update pattern count when model resets
+    Connections {
+        target: patternModel
+        function onModelReset() {
+            patternCount = patternModel.rowCount()
+        }
+    }
+
+    // Check if a pattern is already in the playlist
+    function isPatternInPlaylist(patternName) {
+        // Check original existing patterns
+        if (existingPatterns.indexOf(patternName) !== -1) {
+            return true
+        }
+        // Check patterns added during this session
+        if (sessionAddedPatterns.indexOf(patternName) !== -1) {
+            return true
+        }
+        return false
+    }
+
+    Rectangle {
+        anchors.fill: parent
+        color: Components.ThemeManager.backgroundColor
+    }
+
+    ColumnLayout {
+        anchors.fill: parent
+        spacing: 0
+
+        // Header with back button
+        Rectangle {
+            Layout.fillWidth: true
+            Layout.preferredHeight: 50
+            color: Components.ThemeManager.surfaceColor
+
+            // Bottom border
+            Rectangle {
+                anchors.bottom: parent.bottom
+                width: parent.width
+                height: 1
+                color: Components.ThemeManager.borderColor
+            }
+
+            RowLayout {
+                anchors.fill: parent
+                anchors.leftMargin: 15
+                anchors.rightMargin: 10
+                spacing: 10
+
+                // Back button
+                Button {
+                    text: "← Back"
+                    font.pixelSize: 14
+                    flat: true
+                    visible: !searchExpanded
+                    onClicked: stackView.pop()
+
+                    contentItem: Text {
+                        text: parent.text
+                        font: parent.font
+                        color: Components.ThemeManager.textPrimary
+                        horizontalAlignment: Text.AlignHCenter
+                        verticalAlignment: Text.AlignVCenter
+                    }
+                }
+
+                // Title
+                Label {
+                    text: "Add to \"" + playlistName + "\""
+                    font.pixelSize: 16
+                    font.bold: true
+                    color: Components.ThemeManager.textPrimary
+                    Layout.fillWidth: true
+                    elide: Text.ElideRight
+                    visible: !searchExpanded
+                }
+
+                // Pattern count
+                Label {
+                    text: patternCount + " patterns"
+                    font.pixelSize: 12
+                    color: Components.ThemeManager.textTertiary
+                    visible: !searchExpanded
+                }
+
+                Item {
+                    Layout.fillWidth: true
+                    visible: !searchExpanded
+                }
+
+                // Expandable search (matching ModernPatternListPage)
+                Rectangle {
+                    Layout.fillWidth: searchExpanded
+                    Layout.preferredWidth: searchExpanded ? parent.width - 60 : 120
+                    Layout.preferredHeight: 32
+                    radius: 16
+                    color: searchExpanded ? Components.ThemeManager.surfaceColor : Components.ThemeManager.cardColor
+                    border.color: searchExpanded ? "#2563eb" : Components.ThemeManager.borderColor
+                    border.width: 1
+
+                    Behavior on Layout.preferredWidth {
+                        NumberAnimation { duration: 200 }
+                    }
+
+                    RowLayout {
+                        anchors.fill: parent
+                        anchors.leftMargin: 10
+                        anchors.rightMargin: 10
+                        spacing: 5
+
+                        Text {
+                            text: "⌕"
+                            font.pixelSize: 16
+                            font.family: "sans-serif"
+                            color: searchExpanded ? "#2563eb" : Components.ThemeManager.textSecondary
+                        }
+
+                        TextField {
+                            id: searchField
+                            Layout.fillWidth: true
+                            placeholderText: searchExpanded ? "Search patterns... (press Enter)" : "Search"
+                            font.pixelSize: 14
+                            color: Components.ThemeManager.textPrimary
+                            visible: searchExpanded || text.length > 0
+
+                            property string lastSearchText: ""
+                            property bool hasUnappliedSearch: text !== lastSearchText && text.length > 0
+
+                            background: Rectangle {
+                                color: "transparent"
+                                border.color: searchField.hasUnappliedSearch ? "#f59e0b" : "transparent"
+                                border.width: searchField.hasUnappliedSearch ? 1 : 0
+                                radius: 4
+                            }
+
+                            onAccepted: {
+                                patternModel.filter(text)
+                                lastSearchText = text
+                                Qt.inputMethod.hide()
+                                focus = false
+                            }
+
+                            activeFocusOnPress: true
+                            selectByMouse: true
+                            inputMethodHints: Qt.ImhNoPredictiveText
+
+                            MouseArea {
+                                anchors.fill: parent
+                                onPressed: {
+                                    searchField.forceActiveFocus()
+                                    Qt.inputMethod.show()
+                                    mouse.accepted = false
+                                }
+                            }
+
+                            onActiveFocusChanged: {
+                                if (activeFocus) {
+                                    searchExpanded = true
+                                    Qt.inputMethod.show()
+                                } else {
+                                    if (text !== lastSearchText) {
+                                        patternModel.filter(text)
+                                        lastSearchText = text
+                                    }
+                                }
+                            }
+
+                            Keys.onReturnPressed: {
+                                Qt.inputMethod.hide()
+                                focus = false
+                            }
+
+                            Keys.onEscapePressed: {
+                                text = ""
+                                lastSearchText = ""
+                                patternModel.filter("")
+                                Qt.inputMethod.hide()
+                                focus = false
+                            }
+                        }
+
+                        Text {
+                            text: searchExpanded || searchField.text.length > 0 ? "Search" : ""
+                            font.pixelSize: 12
+                            color: Components.ThemeManager.textTertiary
+                            visible: !searchExpanded && searchField.text.length === 0
+                        }
+                    }
+
+                    MouseArea {
+                        anchors.fill: parent
+                        enabled: !searchExpanded
+                        onClicked: {
+                            searchExpanded = true
+                            searchField.forceActiveFocus()
+                            Qt.inputMethod.show()
+                        }
+                    }
+                }
+
+                // Close button when search expanded
+                Button {
+                    text: "✕"
+                    font.pixelSize: 18
+                    flat: true
+                    visible: searchExpanded
+                    Layout.preferredWidth: 32
+                    Layout.preferredHeight: 32
+                    onClicked: {
+                        searchExpanded = false
+                        searchField.text = ""
+                        searchField.lastSearchText = ""
+                        searchField.focus = false
+                        patternModel.filter("")
+                    }
+                }
+            }
+        }
+
+        // Pattern Grid
+        GridView {
+            id: gridView
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            cellWidth: 200
+            cellHeight: 220
+            model: patternModel
+            clip: true
+
+            ScrollBar.vertical: ScrollBar {
+                active: true
+                policy: ScrollBar.AsNeeded
+            }
+
+            delegate: Item {
+                width: gridView.cellWidth - 10
+                height: gridView.cellHeight - 10
+
+                // Check if pattern is already in playlist
+                property bool isInPlaylist: isPatternInPlaylist(model.name)
+
+                ModernPatternCard {
+                    id: patternCard
+                    anchors.fill: parent
+                    name: model.name
+                    preview: model.preview
+
+                    onClicked: {
+                        // Use the tracking function for immediate visual feedback
+                        page.addPatternToPlaylist(model.name)
+                    }
+                }
+
+                // Selection overlay for patterns already in playlist
+                Rectangle {
+                    anchors.fill: parent
+                    color: "transparent"
+                    border.color: isInPlaylist ? "#2563eb" : "transparent"
+                    border.width: isInPlaylist ? 3 : 0
+                    radius: 12
+
+                    // Checkmark badge for selected patterns
+                    Rectangle {
+                        visible: isInPlaylist
+                        anchors.top: parent.top
+                        anchors.right: parent.right
+                        anchors.topMargin: 12
+                        anchors.rightMargin: 12
+                        width: 28
+                        height: 28
+                        radius: 14
+                        color: "#2563eb"
+
+                        Text {
+                            anchors.centerIn: parent
+                            text: "✓"
+                            font.pixelSize: 16
+                            font.bold: true
+                            color: "white"
+                        }
+                    }
+                }
+            }
+
+            // Add scroll animations
+            add: Transition {
+                NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 300 }
+                NumberAnimation { property: "scale"; from: 0.8; to: 1; duration: 300 }
+            }
+        }
+
+        // Empty state when searching
+        Item {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            visible: patternCount === 0 && searchField.text !== ""
+
+            Column {
+                anchors.centerIn: parent
+                spacing: 20
+
+                Text {
+                    text: "⌕"
+                    font.pixelSize: 48
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    color: Components.ThemeManager.placeholderText
+                }
+
+                Label {
+                    text: "No patterns found"
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    color: Components.ThemeManager.textSecondary
+                    font.pixelSize: 18
+                }
+
+                Label {
+                    text: "Try a different search term"
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    color: Components.ThemeManager.textTertiary
+                    font.pixelSize: 14
+                }
+            }
+        }
+    }
+
+    // Handle pattern added signal for live updates
+    Connections {
+        target: backend
+
+        function onPatternAddedToPlaylist(success, message) {
+            if (success) {
+                console.log("Pattern added to playlist, refreshing selection state")
+                // Extract the pattern name from the message if possible
+                // The message format is typically "Pattern added to playlist"
+                // We'll track additions in sessionAddedPatterns instead
+
+                // Re-trigger binding evaluation by updating the array reference
+                var temp = sessionAddedPatterns.slice()
+                // Try to extract pattern name from recent action
+                // Since we don't get the pattern name directly, we need another approach
+                sessionAddedPatterns = temp
+            }
+        }
+    }
+
+    // Track which pattern was last clicked for visual feedback
+    property string lastClickedPattern: ""
+
+    // Override the click handler to track additions
+    Component.onCompleted: {
+        console.log("PatternSelectorPage loaded for playlist:", playlistName)
+        console.log("Existing patterns:", existingPatterns)
+    }
+
+    // Function to add pattern and track it
+    function addPatternToPlaylist(patternName) {
+        if (!isPatternInPlaylist(patternName) && backend) {
+            backend.addPatternToPlaylist(playlistName, patternName)
+            // Immediately add to session tracking for instant visual feedback
+            var temp = sessionAddedPatterns.slice()
+            temp.push(patternName)
+            sessionAddedPatterns = temp
+        }
+    }
+}

+ 53 - 0
dune-weaver-touch/qml/pages/TableControlPage.qml

@@ -581,6 +581,59 @@ Page {
                     }
                     }
                 }
                 }
 
 
+                // System Controls Section
+                Rectangle {
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: 100
+                    Layout.margins: 5
+                    radius: 8
+                    color: Components.ThemeManager.surfaceColor
+
+                    ColumnLayout {
+                        anchors.fill: parent
+                        anchors.margins: 15
+                        spacing: 10
+
+                        Label {
+                            text: "System Controls"
+                            font.pixelSize: 14
+                            font.bold: true
+                            color: Components.ThemeManager.textPrimary
+                        }
+
+                        RowLayout {
+                            Layout.fillWidth: true
+                            spacing: 8
+
+                            ModernControlButton {
+                                Layout.fillWidth: true
+                                Layout.preferredHeight: 45
+                                text: "Restart Backend"
+                                icon: "↻"
+                                buttonColor: "#f59e0b"
+                                fontSize: 11
+
+                                onClicked: {
+                                    if (backend) backend.restartBackend()
+                                }
+                            }
+
+                            ModernControlButton {
+                                Layout.fillWidth: true
+                                Layout.preferredHeight: 45
+                                text: "Shutdown Pi"
+                                icon: "⏻"
+                                buttonColor: "#dc2626"
+                                fontSize: 11
+
+                                onClicked: {
+                                    if (backend) backend.shutdownPi()
+                                }
+                            }
+                        }
+                    }
+                }
+
                 // Add some bottom spacing for better scrolling
                 // Add some bottom spacing for better scrolling
                 Item {
                 Item {
                     Layout.preferredHeight: 20
                     Layout.preferredHeight: 20