@@ -59,6 +59,17 @@ type ContentPickerCallbacks struct {
5959 // AddToQueue is called when a playable item is selected with ActionAddToQueue.
6060 AddToQueue func (item * kefw2.ContentItem ) error
6161
62+ // AddContainerToQueue is called when Ctrl+a is pressed on a container.
63+ // Should recursively find all tracks and add them to the queue.
64+ // Returns the count of tracks added.
65+ AddContainerToQueue func (item * kefw2.ContentItem ) (int , error )
66+
67+ // DeleteFromQueue is called when Ctrl+d is pressed to delete an item from the queue.
68+ DeleteFromQueue func (item * kefw2.ContentItem ) error
69+
70+ // ClearQueue is called when Ctrl+x is pressed to clear the entire queue.
71+ ClearQueue func () error
72+
6273 // IsPlayable determines if an item is playable (not a container to navigate into).
6374 IsPlayable func (item * kefw2.ContentItem ) bool
6475}
@@ -112,6 +123,7 @@ type ContentPickerConfig struct {
112123 CurrentPath string
113124 Action ContentAction
114125 Callbacks ContentPickerCallbacks
126+ SearchQuery string // Optional: if set, auto-focus on matching item
115127}
116128
117129// NewContentPickerModel creates a new content picker model.
@@ -122,7 +134,7 @@ func NewContentPickerModel(cfg ContentPickerConfig) ContentPickerModel {
122134 ti .CharLimit = 100
123135 ti .Width = 40
124136
125- return ContentPickerModel {
137+ model := ContentPickerModel {
126138 serviceType : cfg .ServiceType ,
127139 callbacks : cfg .Callbacks ,
128140 action : cfg .Action ,
@@ -133,6 +145,20 @@ func NewContentPickerModel(cfg ContentPickerConfig) ContentPickerModel {
133145 title : cfg .Title ,
134146 currentPath : cfg .CurrentPath ,
135147 }
148+
149+ // Auto-focus on matching item if search query provided
150+ if cfg .SearchQuery != "" {
151+ query := strings .TrimSuffix (cfg .SearchQuery , "/" )
152+ for i , item := range cfg .Items {
153+ itemTitle := strings .TrimSuffix (item .Title , "/" )
154+ if strings .EqualFold (itemTitle , query ) {
155+ model .cursor = i
156+ break
157+ }
158+ }
159+ }
160+
161+ return model
136162}
137163
138164// Init implements tea.Model.
@@ -266,21 +292,70 @@ func (m ContentPickerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
266292 return m , tea .Quit
267293 }
268294
269- case "a" :
295+ case "ctrl+ a" :
270296 // Add selected item to queue without exiting
271297 if len (m .filtered ) > 0 && m .cursor < len (m .filtered ) {
272298 selected := & m .filtered [m .cursor ]
273- if m .isItemPlayable (selected ) && m .callbacks .AddToQueue != nil {
274- err := m .callbacks .AddToQueue (selected )
299+ if m .isItemPlayable (selected ) {
300+ // Single track
301+ if m .callbacks .AddToQueue != nil {
302+ err := m .callbacks .AddToQueue (selected )
303+ if err != nil {
304+ m .statusMsg = fmt .Sprintf ("Error: %v" , err )
305+ } else {
306+ m .statusMsg = fmt .Sprintf ("Added to queue: %s" , selected .Title )
307+ }
308+ }
309+ } else {
310+ // Container - add all tracks recursively
311+ if m .callbacks .AddContainerToQueue != nil {
312+ count , err := m .callbacks .AddContainerToQueue (selected )
313+ if err != nil {
314+ m .statusMsg = fmt .Sprintf ("Error: %v" , err )
315+ } else {
316+ m .statusMsg = fmt .Sprintf ("Added %d tracks to queue from: %s" , count , selected .Title )
317+ }
318+ }
319+ }
320+ }
321+ return m , nil
322+
323+ case "ctrl+d" :
324+ // Delete selected item from queue
325+ if len (m .filtered ) > 0 && m .cursor < len (m .filtered ) {
326+ selected := & m .filtered [m .cursor ]
327+ if m .callbacks .DeleteFromQueue != nil {
328+ err := m .callbacks .DeleteFromQueue (selected )
275329 if err != nil {
276330 m .statusMsg = fmt .Sprintf ("Error: %v" , err )
277331 } else {
278- m .statusMsg = fmt .Sprintf ("Added to queue: %s" , selected .Title )
332+ m .statusMsg = fmt .Sprintf ("Deleted: %s" , selected .Title )
333+ // Remove from allItems and filtered
334+ m .allItems = removeItem (m .allItems , selected )
335+ m .filtered = removeItem (m .filtered , selected )
336+ if m .cursor >= len (m .filtered ) && m .cursor > 0 {
337+ m .cursor --
338+ }
279339 }
280340 }
281341 }
282342 return m , nil
283343
344+ case "ctrl+x" :
345+ // Clear entire queue
346+ if m .callbacks .ClearQueue != nil {
347+ err := m .callbacks .ClearQueue ()
348+ if err != nil {
349+ m .statusMsg = fmt .Sprintf ("Error: %v" , err )
350+ } else {
351+ m .statusMsg = "Queue cleared"
352+ m .allItems = nil
353+ m .filtered = nil
354+ m .cursor = 0
355+ }
356+ }
357+ return m , nil
358+
284359 default :
285360 // Handle text input for filtering
286361 var cmd tea.Cmd
@@ -296,6 +371,17 @@ func (m ContentPickerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
296371 return m , cmd
297372}
298373
374+ // removeItem removes an item from a slice by matching Path.
375+ func removeItem (items []kefw2.ContentItem , target * kefw2.ContentItem ) []kefw2.ContentItem {
376+ result := make ([]kefw2.ContentItem , 0 , len (items )- 1 )
377+ for _ , item := range items {
378+ if item .Path != target .Path {
379+ result = append (result , item )
380+ }
381+ }
382+ return result
383+ }
384+
299385// applyFilter filters the items based on the current filter input.
300386func (m * ContentPickerModel ) applyFilter () {
301387 query := m .filterInput .Value ()
@@ -417,11 +503,17 @@ func (m ContentPickerModel) View() string {
417503 case ActionAddToQueue :
418504 actionHint = "add to queue"
419505 }
420- queueHint := ""
506+ extraHints := ""
421507 if m .callbacks .AddToQueue != nil {
422- queueHint = " | a: add to queue"
508+ extraHints + = " | Ctrl+ a: add to queue"
423509 }
424- statusText := fmt .Sprintf ("↑/↓: navigate | Type to filter | Enter: %s%s | Esc: quit" , actionHint , queueHint )
510+ if m .callbacks .DeleteFromQueue != nil {
511+ extraHints += " | Ctrl+d: delete"
512+ }
513+ if m .callbacks .ClearQueue != nil {
514+ extraHints += " | Ctrl+x: clear"
515+ }
516+ statusText := fmt .Sprintf ("↑/↓: navigate | Type to filter | Enter: %s%s | Esc: quit" , actionHint , extraHints )
425517 b .WriteString (m .styles .Status .Render (statusText ))
426518
427519 return b .String ()
@@ -498,8 +590,8 @@ func DefaultPodcastCallbacks(client *kefw2.AirableClient) ContentPickerCallbacks
498590 return client .AddToQueue ([]kefw2.ContentItem {* item }, false )
499591 },
500592 IsPlayable : func (item * kefw2.ContentItem ) bool {
501- // Podcasts (containers with episodes) are navigable, episodes are playable
502- return item .Type != "container" || item . ContainerPlayable
593+ // Only actual audio episodes are playable; all containers should be navigable
594+ return item .Type == "audio"
503595 },
504596 }
505597}
@@ -509,7 +601,8 @@ func DefaultUPnPCallbacks(client *kefw2.AirableClient) ContentPickerCallbacks {
509601 return ContentPickerCallbacks {
510602 Navigate : func (item * kefw2.ContentItem , currentPath string ) ([]kefw2.ContentItem , string , error ) {
511603 // For UPnP, navigate using the item's Path directly
512- resp , err := client .BrowseContainer (item .Path )
604+ // Use BrowseContainerAll to fetch all items (not just first 100)
605+ resp , err := client .BrowseContainerAll (item .Path )
513606 if err != nil {
514607 return nil , "" , err
515608 }
@@ -526,8 +619,26 @@ func DefaultUPnPCallbacks(client *kefw2.AirableClient) ContentPickerCallbacks {
526619 // UPnP supports adding to queue
527620 return client .AddToQueue ([]kefw2.ContentItem {* item }, false )
528621 },
622+ AddContainerToQueue : func (item * kefw2.ContentItem ) (int , error ) {
623+ // Recursively get all tracks from the container
624+ tracks , err := client .GetContainerTracksRecursive (item .Path )
625+ if err != nil {
626+ return 0 , err
627+ }
628+ if len (tracks ) == 0 {
629+ return 0 , fmt .Errorf ("no audio tracks found in container" )
630+ }
631+ err = client .AddToQueue (tracks , false )
632+ if err != nil {
633+ return 0 , err
634+ }
635+ return len (tracks ), nil
636+ },
529637 IsPlayable : func (item * kefw2.ContentItem ) bool {
530- return item .Type != "container" || item .ContainerPlayable
638+ // For UPnP, only audio tracks are directly playable.
639+ // Containers (artists, albums, folders) should always be navigable,
640+ // even if ContainerPlayable is true.
641+ return item .Type == "audio"
531642 },
532643 }
533644}
0 commit comments