diff --git a/backend/mediaprovider/model.go b/backend/mediaprovider/model.go index c865bce..315d3c9 100644 --- a/backend/mediaprovider/model.go +++ b/backend/mediaprovider/model.go @@ -179,4 +179,5 @@ type SearchResult struct { // Unset for ContentTypes Artist, Playlist, and Genre ArtistName string + Query string } diff --git a/ui/controller/controller.go b/ui/controller/controller.go index 84ef147..2d937b4 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -322,49 +322,33 @@ func (m *Controller) PromptForFirstServer() { pop.Show() } -// Show dialog to prompt for playlist. +// Show dialog to select playlist. // Depending on the results of that dialog, potentially create a new playlist // Add tracks to the user-specified playlist func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { - go func() { - pls, err := m.App.ServerManager.Server.GetPlaylists() - pls = sharedutil.FilterSlice(pls, func(pl *mediaprovider.Playlist) bool { - return pl.Owner == m.App.ServerManager.LoggedInUser - }) - if err != nil { - // TODO: surface this error to user - log.Printf("error getting user-owned playlists: %s", err.Error()) - return + sp := dialogs.NewSelectPlaylistDialog(m.App.ServerManager.Server, m.App.ImageManager, m.App.ServerManager.LoggedInUser) + pop := widget.NewModalPopUp(sp.SearchDialog, m.MainWindow.Canvas()) + sp.SetOnDismiss(func() { + pop.Hide() + m.doModalClosed() + }) + sp.SetOnNavigateTo(func(contentType mediaprovider.ContentType, id string, query string) { + pop.Hide() + if id == "" { + go m.App.ServerManager.Server.CreatePlaylist(query, trackIDs) + } else { + m.App.Config.Application.DefaultPlaylistID = id + go m.App.ServerManager.Server.AddPlaylistTracks(id, trackIDs) } - selectedIdx := -1 - plNames := make([]string, 0, len(pls)) - for i, pl := range pls { - plNames = append(plNames, pl.Name) - if defId := m.App.Config.Application.DefaultPlaylistID; defId != "" && pl.ID == defId { - selectedIdx = i - } - } - - dlg := dialogs.NewAddToPlaylistDialog("Add to Playlist", plNames, selectedIdx) - pop := widget.NewModalPopUp(dlg, m.MainWindow.Canvas()) - m.ClosePopUpOnEscape(pop) - dlg.OnCanceled = pop.Hide - dlg.OnSubmit = func(playlistChoice int, newPlaylistName string) { - pop.Hide() - m.doModalClosed() - if playlistChoice < 0 { - go m.App.ServerManager.Server.CreatePlaylist(newPlaylistName, trackIDs) - } else { - playlist := pls[playlistChoice] - m.App.Config.Application.DefaultPlaylistID = playlist.ID - go m.App.ServerManager.Server.AddPlaylistTracks( - playlist.ID, trackIDs) - } - } - m.haveModal = true - pop.Show() - }() + }) + m.ClosePopUpOnEscape(pop) + m.haveModal = true + min := sp.MinSize() + height := fyne.Max(min.Height, fyne.Min(min.Height*1.5, m.MainWindow.Canvas().Size().Height*0.7)) + pop.Resize(fyne.NewSize(min.Width, height)) + pop.Show() + m.MainWindow.Canvas().Focus(sp.GetSearchEntry()) } func (m *Controller) DoEditPlaylistWorkflow(playlist *mediaprovider.Playlist) { @@ -622,7 +606,7 @@ func (c *Controller) ShowQuickSearch() { pop.Hide() c.doModalClosed() }) - qs.SetOnNavigateTo(func(contentType mediaprovider.ContentType, id string) { + qs.SetOnNavigateTo(func(contentType mediaprovider.ContentType, id string, query string) { pop.Hide() c.doModalClosed() switch contentType { diff --git a/ui/dialogs/quicksearch.go b/ui/dialogs/quicksearch.go index 089a301..2b06d1e 100644 --- a/ui/dialogs/quicksearch.go +++ b/ui/dialogs/quicksearch.go @@ -95,7 +95,7 @@ func (q *QuickSearch) SetOnDismiss(onDismiss func()) { q.SearchDialog.OnDismiss = onDismiss } -func (q *QuickSearch) SetOnNavigateTo(onNavigateTo func(mediaprovider.ContentType, string)) { +func (q *QuickSearch) SetOnNavigateTo(onNavigateTo func(mediaprovider.ContentType, string, string)) { q.SearchDialog.OnNavigateTo = onNavigateTo } diff --git a/ui/dialogs/searchdialog.go b/ui/dialogs/searchdialog.go index 2afa2fa..8ddfd9a 100644 --- a/ui/dialogs/searchdialog.go +++ b/ui/dialogs/searchdialog.go @@ -36,7 +36,7 @@ type SearchDialog struct { content *fyne.Container OnDismiss func() - OnNavigateTo func(mediaprovider.ContentType, string) + OnNavigateTo func(mediaprovider.ContentType, string, string) OnSearched func(string) []*mediaprovider.SearchResult OnUpdateSearchResults func(*searchResult, *mediaprovider.SearchResult) } @@ -109,8 +109,9 @@ func (sd *SearchDialog) onSelected(idx int) { } id := sd.searchResults[idx].ID typ := sd.searchResults[idx].Type + query := sd.searchResults[idx].Query sd.resultsMutex.RUnlock() - sd.OnNavigateTo(typ, id) + sd.OnNavigateTo(typ, id, query) } func (sd *SearchDialog) moveSelectionDown() { @@ -164,7 +165,7 @@ func (sd *SearchDialog) update(sr *searchResult, result *mediaprovider.SearchRes if result == nil { return } - if sr.contentType == result.Type && sr.id == result.ID { + if sr.contentType == result.Type && sr.id == result.ID && result.ID != "" { return // nothing to do } sr.id = result.ID diff --git a/ui/dialogs/selectplaylist.go b/ui/dialogs/selectplaylist.go new file mode 100644 index 0000000..c54cf95 --- /dev/null +++ b/ui/dialogs/selectplaylist.go @@ -0,0 +1,127 @@ +package dialogs + +import ( + // "fmt" + "fmt" + "log" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "github.com/deluan/sanitize" + "github.com/dweymouth/supersonic/backend/mediaprovider" + "github.com/dweymouth/supersonic/sharedutil" + "github.com/dweymouth/supersonic/ui/util" +) + +type SelectPlaylist struct { + SearchDialog *SearchDialog + mp mediaprovider.MediaProvider + loggedInUser string +} + +func NewSelectPlaylistDialog(mp mediaprovider.MediaProvider, im util.ImageFetcher, loggedInUser string) *SelectPlaylist { + + sp := &SelectPlaylist{ + mp: mp, + loggedInUser: loggedInUser, + } + + sd := NewSearchDialog( + im, + "Select playlist", + sp.onSearched, + sp.onUpdateSearchResult, + ) + sp.SearchDialog = sd + return sp +} + +func (sp *SelectPlaylist) onSearched(query string) []*mediaprovider.SearchResult { + var results []*mediaprovider.SearchResult + if query != "" { + var filteredPlaylists []*mediaprovider.Playlist + if playlists, err := sp.mp.GetPlaylists(); err != nil { + // TODO: surface this error to user + log.Printf("error getting playlists: %s", err.Error()) + return results + } else { + filteredPlaylists = sharedutil.FilterSlice(playlists, func(playlist *mediaprovider.Playlist) bool { + return strings.Contains( + sanitize.Accents(strings.ToLower(playlist.Name)), + sanitize.Accents(strings.ToLower(query)), + ) && playlist.Owner == sp.loggedInUser + }) + } + + results = append(results, &mediaprovider.SearchResult{ + Name: fmt.Sprintf("Create new playlist: %s", query), + Type: mediaprovider.ContentTypePlaylist, + Query: query, + }) + for _, playlist := range filteredPlaylists { + results = append(results, &mediaprovider.SearchResult{ + Name: playlist.Name, + ID: playlist.ID, + CoverID: playlist.CoverArtID, + Type: mediaprovider.ContentTypePlaylist, + Size: playlist.TrackCount, + ArtistName: playlist.Name, + }) + } + + } + return results +} + +func (sp *SelectPlaylist) onUpdateSearchResult(sr *searchResult, result *mediaprovider.SearchResult) { + if result.ID == "" { + sr.secondary.Segments = []widget.RichTextSegment{} + sr.secondary.Refresh() + return + } + + maybePluralize := func(s string, size int) string { + if size != 1 { + return s + "s" + } + return s + } + secondaryText := fmt.Sprintf("%d %s", result.Size, maybePluralize("track", result.Size)) + sr.secondary.Segments = []widget.RichTextSegment{ + &widget.TextSegment{ + Text: result.Type.String(), + Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, TextStyle: fyne.TextStyle{Bold: true}, Inline: true}, + }, + } + if secondaryText != "" { + sr.secondary.Segments = append(sr.secondary.Segments, + &widget.TextSegment{ + Text: " ยท ", + Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, Inline: true}, + }, + &widget.TextSegment{ + Text: secondaryText, + Style: widget.RichTextStyle{SizeName: theme.SizeNameCaptionText, Inline: true}, + }, + ) + } + sr.secondary.Refresh() +} + +func (sp *SelectPlaylist) SetOnDismiss(onDismiss func()) { + sp.SearchDialog.OnDismiss = onDismiss +} + +func (sp *SelectPlaylist) SetOnNavigateTo(onNavigateTo func(mediaprovider.ContentType, string, string)) { + sp.SearchDialog.OnNavigateTo = onNavigateTo +} + +func (sp *SelectPlaylist) MinSize() fyne.Size { + return sp.SearchDialog.MinSize() +} + +func (sp *SelectPlaylist) GetSearchEntry() fyne.Focusable { + return sp.SearchDialog.SearchEntry +}