From 9a587eee98c03c028bd94929bfff336fb111dd79 Mon Sep 17 00:00:00 2001 From: natilou Date: Thu, 2 May 2024 20:18:15 -0300 Subject: [PATCH 01/14] chore: Improve 'Add to playlists' dialog --- backend/mediaprovider/model.go | 1 + ui/controller/controller.go | 62 ++++++---------- ui/dialogs/quicksearch.go | 2 +- ui/dialogs/searchdialog.go | 7 +- ui/dialogs/selectplaylist.go | 127 +++++++++++++++++++++++++++++++++ 5 files changed, 156 insertions(+), 43 deletions(-) create mode 100644 ui/dialogs/selectplaylist.go 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 +} From 1b53695589c5bf2514256af2ff3f7eb626fbf497 Mon Sep 17 00:00:00 2001 From: natilou Date: Sat, 18 May 2024 12:03:43 -0300 Subject: [PATCH 02/14] add a list with all the playlists and then filter by the search text --- ui/controller/controller.go | 1 + ui/dialogs/quicksearch.go | 1 + ui/dialogs/searchdialog.go | 51 ++++++++++++++++++------- ui/dialogs/selectplaylist.go | 72 +++++++++++++++++++++++------------- 4 files changed, 86 insertions(+), 39 deletions(-) diff --git a/ui/controller/controller.go b/ui/controller/controller.go index 2d937b4..b79a2e2 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -347,6 +347,7 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { 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)) + sp.SearchDialog.Show() pop.Show() m.MainWindow.Canvas().Focus(sp.GetSearchEntry()) } diff --git a/ui/dialogs/quicksearch.go b/ui/dialogs/quicksearch.go index 2b06d1e..0c5d5c6 100644 --- a/ui/dialogs/quicksearch.go +++ b/ui/dialogs/quicksearch.go @@ -27,6 +27,7 @@ func NewQuickSearch(mp mediaprovider.MediaProvider, im util.ImageFetcher) *Quick "Quick Search", q.onSearched, q.onUpdateSearchResult, + nil, ) q.SearchDialog = sd return q diff --git a/ui/dialogs/searchdialog.go b/ui/dialogs/searchdialog.go index 8ddfd9a..4477929 100644 --- a/ui/dialogs/searchdialog.go +++ b/ui/dialogs/searchdialog.go @@ -39,14 +39,16 @@ type SearchDialog struct { OnNavigateTo func(mediaprovider.ContentType, string, string) OnSearched func(string) []*mediaprovider.SearchResult OnUpdateSearchResults func(*searchResult, *mediaprovider.SearchResult) + OnInit func() []*mediaprovider.SearchResult } -func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched func(string) []*mediaprovider.SearchResult, onUpdateSearchResult func(*searchResult, *mediaprovider.SearchResult)) *SearchDialog { +func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched func(string) []*mediaprovider.SearchResult, onUpdateSearchResult func(*searchResult, *mediaprovider.SearchResult), onInit func() []*mediaprovider.SearchResult) *SearchDialog { sd := &SearchDialog{ imgSource: im, loadingDots: widgets.NewLoadingDots(), OnSearched: onSearched, OnUpdateSearchResults: onUpdateSearchResult, + OnInit: onInit, } sd.ExtendBaseWidget(sd) @@ -92,6 +94,11 @@ func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched f return sd } +func (sd *SearchDialog) Show() { + sd.onInit() + sd.BaseWidget.Show() +} + func (sd *SearchDialog) onDismiss() { if sd.OnDismiss != nil { sd.OnDismiss() @@ -132,18 +139,7 @@ func (sd *SearchDialog) moveSelectionUp() { sd.list.Select(sd.selectedIndex) } -func (sd *SearchDialog) onSearched(query string) { - sd.loadingDots.Start() - var results []*mediaprovider.SearchResult - if query != "" { - res := sd.OnSearched(query) - if len(res) == 0 { - log.Println("No results matched the query.") - } else { - results = res - } - } - sd.loadingDots.Stop() +func (sd *SearchDialog) setResults(results []*mediaprovider.SearchResult) { sd.resultsMutex.Lock() sd.searchResults = results sd.resultsMutex.Unlock() @@ -153,6 +149,35 @@ func (sd *SearchDialog) onSearched(query string) { sd.list.Select(0) } +func (sd *SearchDialog) onInit() { + if sd.OnInit == nil { + return + } + sd.loadingDots.Start() + var results []*mediaprovider.SearchResult + res := sd.OnInit() + if len(res) == 0 { + log.Println("No results") + } else { + results = res + } + sd.loadingDots.Stop() + sd.setResults(results) +} + +func (sd *SearchDialog) onSearched(query string) { + sd.loadingDots.Start() + var results []*mediaprovider.SearchResult + res := sd.OnSearched(query) + if len(res) == 0 { + log.Println("No results matched the query.") + } else { + results = res + } + sd.loadingDots.Stop() + sd.setResults(results) +} + func (sd *SearchDialog) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(sd.content) } diff --git a/ui/dialogs/selectplaylist.go b/ui/dialogs/selectplaylist.go index c54cf95..b4f358c 100644 --- a/ui/dialogs/selectplaylist.go +++ b/ui/dialogs/selectplaylist.go @@ -19,6 +19,7 @@ type SelectPlaylist struct { SearchDialog *SearchDialog mp mediaprovider.MediaProvider loggedInUser string + allPlaylists []*mediaprovider.Playlist } func NewSelectPlaylistDialog(mp mediaprovider.MediaProvider, im util.ImageFetcher, loggedInUser string) *SelectPlaylist { @@ -27,50 +28,69 @@ func NewSelectPlaylistDialog(mp mediaprovider.MediaProvider, im util.ImageFetche mp: mp, loggedInUser: loggedInUser, } - sd := NewSearchDialog( im, "Select playlist", sp.onSearched, sp.onUpdateSearchResult, + sp.onInit, ) sp.SearchDialog = sd return sp } +func (sp *SelectPlaylist) onInit() []*mediaprovider.SearchResult { + var results []*mediaprovider.SearchResult + playlists, err := sp.mp.GetPlaylists() + if err != nil { + // TODO: surface this error to user + log.Printf("error getting playlists: %s", err.Error()) + return results + } + sp.allPlaylists = sharedutil.FilterSlice(playlists, func(playlist *mediaprovider.Playlist) bool { + return playlist.Owner == sp.loggedInUser + }) + for _, playlist := range sp.allPlaylists { + 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) 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 - }) - } - + var filteredPlaylists []*mediaprovider.Playlist + if query == "" { + filteredPlaylists = sp.allPlaylists + } else { + filteredPlaylists = sharedutil.FilterSlice(sp.allPlaylists, func(playlist *mediaprovider.Playlist) bool { + return strings.Contains( + sanitize.Accents(strings.ToLower(playlist.Name)), + sanitize.Accents(strings.ToLower(query)), + ) + }) 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, - }) - } + } + 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 } From d939c7f85f4c1d251868c4a1ff4b5c15c87acc74 Mon Sep 17 00:00:00 2001 From: natilou Date: Sat, 18 May 2024 16:03:21 -0300 Subject: [PATCH 03/14] add 'Skip duplicates' option --- ui/controller/controller.go | 26 +++++++++++++++++-- ui/dialogs/searchdialog.go | 48 +++++++++++++++++++++++++----------- ui/dialogs/selectplaylist.go | 11 ++++++--- 3 files changed, 65 insertions(+), 20 deletions(-) diff --git a/ui/controller/controller.go b/ui/controller/controller.go index b79a2e2..02e7e97 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -12,6 +12,7 @@ import ( "os" "path/filepath" "time" + "slices" "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" @@ -338,7 +339,27 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { go m.App.ServerManager.Server.CreatePlaylist(query, trackIDs) } else { m.App.Config.Application.DefaultPlaylistID = id - go m.App.ServerManager.Server.AddPlaylistTracks(id, trackIDs) + if sp.SkipDuplicates { + var filterTrackIDs []string + go func() { + if selectedPlaylist, err := m.App.ServerManager.Server.GetPlaylist(id); err != nil { + log.Printf("error getting playlist: %s", err.Error()) + } else { + var trackIDsInPaylist []string + + for _, track := range selectedPlaylist.Tracks{ + trackIDsInPaylist = append(trackIDsInPaylist, track.ID) + } + filterTrackIDs = sharedutil.FilterSlice(trackIDs, func(trackID string) bool { + return !slices.Contains(trackIDsInPaylist, trackID) + }) + + } + m.App.ServerManager.Server.AddPlaylistTracks(id, filterTrackIDs) + }() + } else { + go m.App.ServerManager.Server.AddPlaylistTracks(id, trackIDs) + } } }) @@ -346,8 +367,8 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { 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)) sp.SearchDialog.Show() + pop.Resize(fyne.NewSize(min.Width, height)) pop.Show() m.MainWindow.Canvas().Focus(sp.GetSearchEntry()) } @@ -627,6 +648,7 @@ func (c *Controller) ShowQuickSearch() { c.haveModal = true min := qs.MinSize() height := fyne.Max(min.Height, fyne.Min(min.Height*1.5, c.MainWindow.Canvas().Size().Height*0.7)) + qs.SearchDialog.Show() pop.Resize(fyne.NewSize(min.Width, height)) pop.Show() c.MainWindow.Canvas().Focus(qs.GetSearchEntry()) diff --git a/ui/dialogs/searchdialog.go b/ui/dialogs/searchdialog.go index 4477929..0272cae 100644 --- a/ui/dialogs/searchdialog.go +++ b/ui/dialogs/searchdialog.go @@ -33,22 +33,24 @@ type SearchDialog struct { list *widget.List selectedIndex int + placeholderTitle string content *fyne.Container OnDismiss func() OnNavigateTo func(mediaprovider.ContentType, string, string) OnSearched func(string) []*mediaprovider.SearchResult OnUpdateSearchResults func(*searchResult, *mediaprovider.SearchResult) - OnInit func() []*mediaprovider.SearchResult + OnInit func() ([]*mediaprovider.SearchResult, *widget.Check) } -func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched func(string) []*mediaprovider.SearchResult, onUpdateSearchResult func(*searchResult, *mediaprovider.SearchResult), onInit func() []*mediaprovider.SearchResult) *SearchDialog { +func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched func(string) []*mediaprovider.SearchResult, onUpdateSearchResult func(*searchResult, *mediaprovider.SearchResult), onInit func() ([]*mediaprovider.SearchResult, *widget.Check)) *SearchDialog { sd := &SearchDialog{ imgSource: im, loadingDots: widgets.NewLoadingDots(), OnSearched: onSearched, OnUpdateSearchResults: onUpdateSearchResult, OnInit: onInit, + placeholderTitle: placeholderTitle, } sd.ExtendBaseWidget(sd) @@ -80,17 +82,6 @@ func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched f sd.update(sr, result) }, ) - - dismissBtn := widget.NewButton("Close", sd.onDismiss) - title := widget.NewRichText(&widget.TextSegment{Text: placeholderTitle, Style: util.BoldRichTextStyle}) - title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter - sd.content = container.NewStack( - container.NewBorder( - container.NewVBox(title, se), - container.NewVBox(widget.NewSeparator(), container.NewHBox(layout.NewSpacer(), dismissBtn)), - nil, nil, sd.list), - container.NewCenter(sd.loadingDots), - ) return sd } @@ -149,20 +140,47 @@ func (sd *SearchDialog) setResults(results []*mediaprovider.SearchResult) { sd.list.Select(0) } +func (sd *SearchDialog) SetContent(checkBox *widget.Check) { + dismissBtn := widget.NewButton("Close", sd.onDismiss) + title := widget.NewRichText(&widget.TextSegment{Text:sd.placeholderTitle, Style: util.BoldRichTextStyle}) + title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter + se := sd.SearchEntry.(fyne.CanvasObject) + if checkBox != nil { + sd.content = container.NewStack( + container.NewBorder( + container.NewVBox(title, se), + container.NewVBox(widget.NewSeparator(), container.NewHBox(checkBox, layout.NewSpacer(), dismissBtn)), + nil, nil, sd.list), + container.NewCenter(sd.loadingDots), + ) + } else { + sd.content = container.NewStack( + container.NewBorder( + container.NewVBox(title, se), + container.NewVBox(widget.NewSeparator(), container.NewHBox(layout.NewSpacer(), dismissBtn)), + nil, nil, sd.list), + container.NewCenter(sd.loadingDots), + ) + } +} + + func (sd *SearchDialog) onInit() { if sd.OnInit == nil { + sd.SetContent(nil) return } sd.loadingDots.Start() var results []*mediaprovider.SearchResult - res := sd.OnInit() + res, checkBox := sd.OnInit() if len(res) == 0 { log.Println("No results") } else { results = res } + sd.SetContent(checkBox) sd.loadingDots.Stop() - sd.setResults(results) + sd.setResults(results) } func (sd *SearchDialog) onSearched(query string) { diff --git a/ui/dialogs/selectplaylist.go b/ui/dialogs/selectplaylist.go index b4f358c..abf5838 100644 --- a/ui/dialogs/selectplaylist.go +++ b/ui/dialogs/selectplaylist.go @@ -20,6 +20,7 @@ type SelectPlaylist struct { mp mediaprovider.MediaProvider loggedInUser string allPlaylists []*mediaprovider.Playlist + SkipDuplicates bool } func NewSelectPlaylistDialog(mp mediaprovider.MediaProvider, im util.ImageFetcher, loggedInUser string) *SelectPlaylist { @@ -27,6 +28,7 @@ func NewSelectPlaylistDialog(mp mediaprovider.MediaProvider, im util.ImageFetche sp := &SelectPlaylist{ mp: mp, loggedInUser: loggedInUser, + SkipDuplicates: false, } sd := NewSearchDialog( im, @@ -39,13 +41,13 @@ func NewSelectPlaylistDialog(mp mediaprovider.MediaProvider, im util.ImageFetche return sp } -func (sp *SelectPlaylist) onInit() []*mediaprovider.SearchResult { +func (sp *SelectPlaylist) onInit() ([]*mediaprovider.SearchResult, *widget.Check) { var results []*mediaprovider.SearchResult playlists, err := sp.mp.GetPlaylists() if err != nil { // TODO: surface this error to user log.Printf("error getting playlists: %s", err.Error()) - return results + return results, nil } sp.allPlaylists = sharedutil.FilterSlice(playlists, func(playlist *mediaprovider.Playlist) bool { return playlist.Owner == sp.loggedInUser @@ -60,7 +62,10 @@ func (sp *SelectPlaylist) onInit() []*mediaprovider.SearchResult { ArtistName: playlist.Name, }) } - return results + skipDuplicatesCheck := widget.NewCheck("Skip duplicates", func(checked bool) { + sp.SkipDuplicates = checked + }) + return results, skipDuplicatesCheck } func (sp *SelectPlaylist) onSearched(query string) []*mediaprovider.SearchResult { From 090874591af7877b904c24a5a85672b6632d6964 Mon Sep 17 00:00:00 2001 From: natilou Date: Sat, 18 May 2024 16:07:50 -0300 Subject: [PATCH 04/14] go fmt --- ui/controller/controller.go | 10 +++++----- ui/dialogs/searchdialog.go | 21 ++++++++++----------- ui/dialogs/selectplaylist.go | 12 ++++++------ 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/ui/controller/controller.go b/ui/controller/controller.go index 02e7e97..a659483 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -11,8 +11,8 @@ import ( "net/url" "os" "path/filepath" - "time" "slices" + "time" "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" @@ -346,20 +346,20 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { log.Printf("error getting playlist: %s", err.Error()) } else { var trackIDsInPaylist []string - - for _, track := range selectedPlaylist.Tracks{ + + for _, track := range selectedPlaylist.Tracks { trackIDsInPaylist = append(trackIDsInPaylist, track.ID) } filterTrackIDs = sharedutil.FilterSlice(trackIDs, func(trackID string) bool { return !slices.Contains(trackIDsInPaylist, trackID) }) - + } m.App.ServerManager.Server.AddPlaylistTracks(id, filterTrackIDs) }() } else { go m.App.ServerManager.Server.AddPlaylistTracks(id, trackIDs) - } + } } }) diff --git a/ui/dialogs/searchdialog.go b/ui/dialogs/searchdialog.go index 0272cae..9677268 100644 --- a/ui/dialogs/searchdialog.go +++ b/ui/dialogs/searchdialog.go @@ -33,8 +33,8 @@ type SearchDialog struct { list *widget.List selectedIndex int - placeholderTitle string - content *fyne.Container + placeholderTitle string + content *fyne.Container OnDismiss func() OnNavigateTo func(mediaprovider.ContentType, string, string) @@ -142,7 +142,7 @@ func (sd *SearchDialog) setResults(results []*mediaprovider.SearchResult) { func (sd *SearchDialog) SetContent(checkBox *widget.Check) { dismissBtn := widget.NewButton("Close", sd.onDismiss) - title := widget.NewRichText(&widget.TextSegment{Text:sd.placeholderTitle, Style: util.BoldRichTextStyle}) + title := widget.NewRichText(&widget.TextSegment{Text: sd.placeholderTitle, Style: util.BoldRichTextStyle}) title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter se := sd.SearchEntry.(fyne.CanvasObject) if checkBox != nil { @@ -155,15 +155,14 @@ func (sd *SearchDialog) SetContent(checkBox *widget.Check) { ) } else { sd.content = container.NewStack( - container.NewBorder( - container.NewVBox(title, se), - container.NewVBox(widget.NewSeparator(), container.NewHBox(layout.NewSpacer(), dismissBtn)), - nil, nil, sd.list), - container.NewCenter(sd.loadingDots), - ) + container.NewBorder( + container.NewVBox(title, se), + container.NewVBox(widget.NewSeparator(), container.NewHBox(layout.NewSpacer(), dismissBtn)), + nil, nil, sd.list), + container.NewCenter(sd.loadingDots), + ) } } - func (sd *SearchDialog) onInit() { if sd.OnInit == nil { @@ -180,7 +179,7 @@ func (sd *SearchDialog) onInit() { } sd.SetContent(checkBox) sd.loadingDots.Stop() - sd.setResults(results) + sd.setResults(results) } func (sd *SearchDialog) onSearched(query string) { diff --git a/ui/dialogs/selectplaylist.go b/ui/dialogs/selectplaylist.go index abf5838..9136d48 100644 --- a/ui/dialogs/selectplaylist.go +++ b/ui/dialogs/selectplaylist.go @@ -16,18 +16,18 @@ import ( ) type SelectPlaylist struct { - SearchDialog *SearchDialog - mp mediaprovider.MediaProvider - loggedInUser string - allPlaylists []*mediaprovider.Playlist + SearchDialog *SearchDialog + mp mediaprovider.MediaProvider + loggedInUser string + allPlaylists []*mediaprovider.Playlist SkipDuplicates bool } func NewSelectPlaylistDialog(mp mediaprovider.MediaProvider, im util.ImageFetcher, loggedInUser string) *SelectPlaylist { sp := &SelectPlaylist{ - mp: mp, - loggedInUser: loggedInUser, + mp: mp, + loggedInUser: loggedInUser, SkipDuplicates: false, } sd := NewSearchDialog( From b040df3a182913ba1b94972d621571f13836a5c3 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 23 May 2024 15:45:42 -0700 Subject: [PATCH 05/14] Refactor: move update logic back to base type --- ui/dialogs/quicksearch.go | 51 ------------------- ui/dialogs/searchdialog.go | 99 +++++++++++++++++++++++++----------- ui/dialogs/selectplaylist.go | 37 -------------- 3 files changed, 70 insertions(+), 117 deletions(-) diff --git a/ui/dialogs/quicksearch.go b/ui/dialogs/quicksearch.go index 0c5d5c6..b3eedde 100644 --- a/ui/dialogs/quicksearch.go +++ b/ui/dialogs/quicksearch.go @@ -1,12 +1,9 @@ package dialogs import ( - "fmt" "log" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/theme" - "fyne.io/fyne/v2/widget" "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/ui/util" ) @@ -26,7 +23,6 @@ func NewQuickSearch(mp mediaprovider.MediaProvider, im util.ImageFetcher) *Quick im, "Quick Search", q.onSearched, - q.onUpdateSearchResult, nil, ) q.SearchDialog = sd @@ -45,53 +41,6 @@ func (q *QuickSearch) onSearched(query string) []*mediaprovider.SearchResult { return results } -func (q *QuickSearch) onUpdateSearchResult(sr *searchResult, result *mediaprovider.SearchResult) { - - maybePluralize := func(s string, size int) string { - if size != 1 { - return s + "s" - } - return s - } - - var secondaryText string - switch result.Type { - case mediaprovider.ContentTypeAlbum: - secondaryText = result.ArtistName - case mediaprovider.ContentTypeArtist: - secondaryText = fmt.Sprintf("%d %s", result.Size, maybePluralize("album", result.Size)) - case mediaprovider.ContentTypeTrack: - secondaryText = result.ArtistName - case mediaprovider.ContentTypePlaylist: - secondaryText = fmt.Sprintf("%d %s", result.Size, maybePluralize("track", result.Size)) - case mediaprovider.ContentTypeGenre: - if result.Size > 0 { - secondaryText = fmt.Sprintf("%d %s", result.Size, maybePluralize("album", result.Size)) - } else { - secondaryText = "" - } - } - 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 (q *QuickSearch) SetOnDismiss(onDismiss func()) { q.SearchDialog.OnDismiss = onDismiss } diff --git a/ui/dialogs/searchdialog.go b/ui/dialogs/searchdialog.go index 9677268..d142eba 100644 --- a/ui/dialogs/searchdialog.go +++ b/ui/dialogs/searchdialog.go @@ -1,6 +1,7 @@ package dialogs import ( + "fmt" "image" "log" "sync" @@ -36,21 +37,19 @@ type SearchDialog struct { placeholderTitle string content *fyne.Container - OnDismiss func() - OnNavigateTo func(mediaprovider.ContentType, string, string) - OnSearched func(string) []*mediaprovider.SearchResult - OnUpdateSearchResults func(*searchResult, *mediaprovider.SearchResult) - OnInit func() ([]*mediaprovider.SearchResult, *widget.Check) + OnDismiss func() + OnNavigateTo func(mediaprovider.ContentType, string, string) + OnSearched func(string) []*mediaprovider.SearchResult + OnInit func() ([]*mediaprovider.SearchResult, *widget.Check) } -func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched func(string) []*mediaprovider.SearchResult, onUpdateSearchResult func(*searchResult, *mediaprovider.SearchResult), onInit func() ([]*mediaprovider.SearchResult, *widget.Check)) *SearchDialog { +func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched func(string) []*mediaprovider.SearchResult, onInit func() ([]*mediaprovider.SearchResult, *widget.Check)) *SearchDialog { sd := &SearchDialog{ - imgSource: im, - loadingDots: widgets.NewLoadingDots(), - OnSearched: onSearched, - OnUpdateSearchResults: onUpdateSearchResult, - OnInit: onInit, - placeholderTitle: placeholderTitle, + imgSource: im, + loadingDots: widgets.NewLoadingDots(), + OnSearched: onSearched, + OnInit: onInit, + placeholderTitle: placeholderTitle, } sd.ExtendBaseWidget(sd) @@ -79,7 +78,7 @@ func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched f sd.resultsMutex.RUnlock() sr := co.(*searchResult) sr.index = lii - sd.update(sr, result) + sr.Update(result) }, ) return sd @@ -203,22 +202,6 @@ func (sd *SearchDialog) MinSize() fyne.Size { return fyne.NewSize(400, 350) } -func (sd *SearchDialog) update(sr *searchResult, result *mediaprovider.SearchResult) { - if result == nil { - return - } - if sr.contentType == result.Type && sr.id == result.ID && result.ID != "" { - return // nothing to do - } - sr.id = result.ID - sr.contentType = result.Type - sr.image.CenterIcon = placeholderIconForContentType(result.Type) - sr.imageLoader.Load(result.CoverID) - sr.title.SetText(result.Name) - - sd.OnUpdateSearchResults(sr, result) -} - func placeholderIconForContentType(c mediaprovider.ContentType) fyne.Resource { switch c { case mediaprovider.ContentTypeAlbum: @@ -274,6 +257,64 @@ func newSearchResult(parent *SearchDialog) *searchResult { return qs } +func (s *searchResult) Update(result *mediaprovider.SearchResult) { + if result == nil { + return + } + if s.contentType == result.Type && s.id == result.ID { + return // nothing to do + } + s.id = result.ID + s.contentType = result.Type + s.image.CenterIcon = placeholderIconForContentType(result.Type) + s.imageLoader.Load(result.CoverID) + s.title.SetText(result.Name) + + maybePluralize := func(s string, size int) string { + if size != 1 { + return s + "s" + } + return s + } + var secondaryText string + switch result.Type { + case mediaprovider.ContentTypeAlbum: + secondaryText = result.ArtistName + case mediaprovider.ContentTypeArtist: + secondaryText = fmt.Sprintf("%d %s", result.Size, maybePluralize("album", result.Size)) + case mediaprovider.ContentTypeTrack: + secondaryText = result.ArtistName + case mediaprovider.ContentTypePlaylist: + secondaryText = fmt.Sprintf("%d %s", result.Size, maybePluralize("track", result.Size)) + case mediaprovider.ContentTypeGenre: + if result.Size > 0 { + secondaryText = fmt.Sprintf("%d %s", result.Size, maybePluralize("album", result.Size)) + } else { + secondaryText = "" + } + } + s.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 != "" { + s.secondary.Segments = append(s.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}, + }, + ) + } + + s.secondary.Refresh() +} + func (q *searchResult) Tapped(_ *fyne.PointEvent) { q.parent.onSelected(q.index) } diff --git a/ui/dialogs/selectplaylist.go b/ui/dialogs/selectplaylist.go index 9136d48..12c412c 100644 --- a/ui/dialogs/selectplaylist.go +++ b/ui/dialogs/selectplaylist.go @@ -7,7 +7,6 @@ import ( "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" @@ -34,7 +33,6 @@ func NewSelectPlaylistDialog(mp mediaprovider.MediaProvider, im util.ImageFetche im, "Select playlist", sp.onSearched, - sp.onUpdateSearchResult, sp.onInit, ) sp.SearchDialog = sd @@ -100,41 +98,6 @@ func (sp *SelectPlaylist) onSearched(query string) []*mediaprovider.SearchResult 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 } From d43b4a4c8fdc06eb9fe3b2067ebfc3c1ec97d803 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 23 May 2024 15:50:19 -0700 Subject: [PATCH 06/14] unexport SearchEntry --- ui/dialogs/quicksearch.go | 2 +- ui/dialogs/searchdialog.go | 35 ++++++++++++++++++----------------- ui/dialogs/selectplaylist.go | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/ui/dialogs/quicksearch.go b/ui/dialogs/quicksearch.go index b3eedde..5b9f2ea 100644 --- a/ui/dialogs/quicksearch.go +++ b/ui/dialogs/quicksearch.go @@ -54,5 +54,5 @@ func (q *QuickSearch) MinSize() fyne.Size { } func (q *QuickSearch) GetSearchEntry() fyne.Focusable { - return q.SearchDialog.SearchEntry + return q.SearchDialog.GetSearchEntry() } diff --git a/ui/dialogs/searchdialog.go b/ui/dialogs/searchdialog.go index d142eba..284e60f 100644 --- a/ui/dialogs/searchdialog.go +++ b/ui/dialogs/searchdialog.go @@ -24,23 +24,21 @@ import ( type SearchDialog struct { widget.BaseWidget - SearchEntry fyne.Focusable // exported so it can be focused by the Controller - - imgSource util.ImageFetcher - - resultsMutex sync.RWMutex - searchResults []*mediaprovider.SearchResult - loadingDots *widgets.LoadingDots - list *widget.List - selectedIndex int - - placeholderTitle string - content *fyne.Container - OnDismiss func() OnNavigateTo func(mediaprovider.ContentType, string, string) OnSearched func(string) []*mediaprovider.SearchResult OnInit func() ([]*mediaprovider.SearchResult, *widget.Check) + + imgSource util.ImageFetcher + resultsMutex sync.RWMutex + searchResults []*mediaprovider.SearchResult + selectedIndex int + + searchEntry *searchEntry + loadingDots *widgets.LoadingDots + list *widget.List + placeholderTitle string + content *fyne.Container } func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched func(string) []*mediaprovider.SearchResult, onInit func() ([]*mediaprovider.SearchResult, *widget.Check)) *SearchDialog { @@ -61,7 +59,7 @@ func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched f se.OnTypedDown = sd.moveSelectionDown se.OnTypedUp = sd.moveSelectionUp se.OnTypedEscape = sd.onDismiss - sd.SearchEntry = se + sd.searchEntry = se sd.list = widget.NewList( func() int { sd.resultsMutex.RLock() @@ -84,6 +82,10 @@ func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched f return sd } +func (sd *SearchDialog) GetSearchEntry() fyne.Focusable { + return sd.searchEntry +} + func (sd *SearchDialog) Show() { sd.onInit() sd.BaseWidget.Show() @@ -143,11 +145,10 @@ func (sd *SearchDialog) SetContent(checkBox *widget.Check) { dismissBtn := widget.NewButton("Close", sd.onDismiss) title := widget.NewRichText(&widget.TextSegment{Text: sd.placeholderTitle, Style: util.BoldRichTextStyle}) title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter - se := sd.SearchEntry.(fyne.CanvasObject) if checkBox != nil { sd.content = container.NewStack( container.NewBorder( - container.NewVBox(title, se), + container.NewVBox(title, sd.searchEntry), container.NewVBox(widget.NewSeparator(), container.NewHBox(checkBox, layout.NewSpacer(), dismissBtn)), nil, nil, sd.list), container.NewCenter(sd.loadingDots), @@ -155,7 +156,7 @@ func (sd *SearchDialog) SetContent(checkBox *widget.Check) { } else { sd.content = container.NewStack( container.NewBorder( - container.NewVBox(title, se), + container.NewVBox(title, sd.searchEntry), container.NewVBox(widget.NewSeparator(), container.NewHBox(layout.NewSpacer(), dismissBtn)), nil, nil, sd.list), container.NewCenter(sd.loadingDots), diff --git a/ui/dialogs/selectplaylist.go b/ui/dialogs/selectplaylist.go index 12c412c..a4564b8 100644 --- a/ui/dialogs/selectplaylist.go +++ b/ui/dialogs/selectplaylist.go @@ -111,5 +111,5 @@ func (sp *SelectPlaylist) MinSize() fyne.Size { } func (sp *SelectPlaylist) GetSearchEntry() fyne.Focusable { - return sp.SearchDialog.SearchEntry + return sp.SearchDialog.GetSearchEntry() } From 21feca32652cefc5f9b3bbe35692a1e8f5739444 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 23 May 2024 15:56:56 -0700 Subject: [PATCH 07/14] remove Query from mediaprovider.SearchResult --- backend/mediaprovider/model.go | 1 - ui/controller/controller.go | 8 ++++---- ui/dialogs/quicksearch.go | 2 +- ui/dialogs/searchdialog.go | 11 ++++++++--- ui/dialogs/selectplaylist.go | 7 +++---- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/backend/mediaprovider/model.go b/backend/mediaprovider/model.go index 315d3c9..c865bce 100644 --- a/backend/mediaprovider/model.go +++ b/backend/mediaprovider/model.go @@ -179,5 +179,4 @@ 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 a659483..9c4aea0 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -333,10 +333,10 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { pop.Hide() m.doModalClosed() }) - sp.SetOnNavigateTo(func(contentType mediaprovider.ContentType, id string, query string) { + sp.SetOnNavigateTo(func(contentType mediaprovider.ContentType, id string) { pop.Hide() - if id == "" { - go m.App.ServerManager.Server.CreatePlaylist(query, trackIDs) + if id == "" /* creating new playlist */ { + go m.App.ServerManager.Server.CreatePlaylist(sp.SearchDialog.SearchQuery(), trackIDs) } else { m.App.Config.Application.DefaultPlaylistID = id if sp.SkipDuplicates { @@ -628,7 +628,7 @@ func (c *Controller) ShowQuickSearch() { pop.Hide() c.doModalClosed() }) - qs.SetOnNavigateTo(func(contentType mediaprovider.ContentType, id string, query string) { + qs.SetOnNavigateTo(func(contentType mediaprovider.ContentType, id string) { pop.Hide() c.doModalClosed() switch contentType { diff --git a/ui/dialogs/quicksearch.go b/ui/dialogs/quicksearch.go index 5b9f2ea..f75fc6d 100644 --- a/ui/dialogs/quicksearch.go +++ b/ui/dialogs/quicksearch.go @@ -45,7 +45,7 @@ func (q *QuickSearch) SetOnDismiss(onDismiss func()) { q.SearchDialog.OnDismiss = onDismiss } -func (q *QuickSearch) SetOnNavigateTo(onNavigateTo func(mediaprovider.ContentType, string, string)) { +func (q *QuickSearch) SetOnNavigateTo(onNavigateTo func(mediaprovider.ContentType, string)) { q.SearchDialog.OnNavigateTo = onNavigateTo } diff --git a/ui/dialogs/searchdialog.go b/ui/dialogs/searchdialog.go index 284e60f..cfdadee 100644 --- a/ui/dialogs/searchdialog.go +++ b/ui/dialogs/searchdialog.go @@ -25,7 +25,7 @@ type SearchDialog struct { widget.BaseWidget OnDismiss func() - OnNavigateTo func(mediaprovider.ContentType, string, string) + OnNavigateTo func(mediaprovider.ContentType, string) OnSearched func(string) []*mediaprovider.SearchResult OnInit func() ([]*mediaprovider.SearchResult, *widget.Check) @@ -82,10 +82,16 @@ func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched f return sd } +// GetSearchEntry returns the search Entry widget for focusing func (sd *SearchDialog) GetSearchEntry() fyne.Focusable { return sd.searchEntry } +// SearchQuery returns the current search query entered by the user +func (sd *SearchDialog) SearchQuery() string { + return sd.searchEntry.Text +} + func (sd *SearchDialog) Show() { sd.onInit() sd.BaseWidget.Show() @@ -108,9 +114,8 @@ 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, query) + sd.OnNavigateTo(typ, id) } func (sd *SearchDialog) moveSelectionDown() { diff --git a/ui/dialogs/selectplaylist.go b/ui/dialogs/selectplaylist.go index a4564b8..5de46d0 100644 --- a/ui/dialogs/selectplaylist.go +++ b/ui/dialogs/selectplaylist.go @@ -79,9 +79,8 @@ func (sp *SelectPlaylist) onSearched(query string) []*mediaprovider.SearchResult ) }) results = append(results, &mediaprovider.SearchResult{ - Name: fmt.Sprintf("Create new playlist: %s", query), - Type: mediaprovider.ContentTypePlaylist, - Query: query, + Name: fmt.Sprintf("Create new playlist: %s", query), + Type: mediaprovider.ContentTypePlaylist, }) } @@ -102,7 +101,7 @@ func (sp *SelectPlaylist) SetOnDismiss(onDismiss func()) { sp.SearchDialog.OnDismiss = onDismiss } -func (sp *SelectPlaylist) SetOnNavigateTo(onNavigateTo func(mediaprovider.ContentType, string, string)) { +func (sp *SelectPlaylist) SetOnNavigateTo(onNavigateTo func(mediaprovider.ContentType, string)) { sp.SearchDialog.OnNavigateTo = onNavigateTo } From b999d94e724639902c723ce4828fe9447e5d9bb1 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 23 May 2024 16:09:26 -0700 Subject: [PATCH 08/14] simplify onSearched --- ui/dialogs/selectplaylist.go | 61 +++++++++++++++++------------------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/ui/dialogs/selectplaylist.go b/ui/dialogs/selectplaylist.go index 5de46d0..53f2a86 100644 --- a/ui/dialogs/selectplaylist.go +++ b/ui/dialogs/selectplaylist.go @@ -15,11 +15,11 @@ import ( ) type SelectPlaylist struct { - SearchDialog *SearchDialog - mp mediaprovider.MediaProvider - loggedInUser string - allPlaylists []*mediaprovider.Playlist - SkipDuplicates bool + SearchDialog *SearchDialog + mp mediaprovider.MediaProvider + loggedInUser string + allPlaylistResuts []*mediaprovider.SearchResult + SkipDuplicates bool } func NewSelectPlaylistDialog(mp mediaprovider.MediaProvider, im util.ImageFetcher, loggedInUser string) *SelectPlaylist { @@ -47,53 +47,48 @@ func (sp *SelectPlaylist) onInit() ([]*mediaprovider.SearchResult, *widget.Check log.Printf("error getting playlists: %s", err.Error()) return results, nil } - sp.allPlaylists = sharedutil.FilterSlice(playlists, func(playlist *mediaprovider.Playlist) bool { + userPlaylists := sharedutil.FilterSlice(playlists, func(playlist *mediaprovider.Playlist) bool { return playlist.Owner == sp.loggedInUser }) - for _, playlist := range sp.allPlaylists { - results = append(results, &mediaprovider.SearchResult{ - Name: playlist.Name, - ID: playlist.ID, - CoverID: playlist.CoverArtID, - Type: mediaprovider.ContentTypePlaylist, - Size: playlist.TrackCount, - ArtistName: playlist.Name, - }) - } + sp.allPlaylistResuts = sharedutil.MapSlice(userPlaylists, sp.playlistToSearchResult) skipDuplicatesCheck := widget.NewCheck("Skip duplicates", func(checked bool) { sp.SkipDuplicates = checked }) return results, skipDuplicatesCheck } +func (sp *SelectPlaylist) playlistToSearchResult(playlist *mediaprovider.Playlist) *mediaprovider.SearchResult { + if playlist == nil { + return nil + } + return &mediaprovider.SearchResult{ + Name: playlist.Name, + ID: playlist.ID, + CoverID: playlist.CoverArtID, + Type: mediaprovider.ContentTypePlaylist, + Size: playlist.TrackCount, + ArtistName: playlist.Name, + } +} + func (sp *SelectPlaylist) onSearched(query string) []*mediaprovider.SearchResult { var results []*mediaprovider.SearchResult - var filteredPlaylists []*mediaprovider.Playlist if query == "" { - filteredPlaylists = sp.allPlaylists + results = sp.allPlaylistResuts } else { - filteredPlaylists = sharedutil.FilterSlice(sp.allPlaylists, func(playlist *mediaprovider.Playlist) bool { + results = []*mediaprovider.SearchResult{{ + Name: fmt.Sprintf("Create new playlist: %s", query), + Type: mediaprovider.ContentTypePlaylist, + }} + filteredPlaylists := sharedutil.FilterSlice(sp.allPlaylistResuts, func(playlist *mediaprovider.SearchResult) bool { return strings.Contains( sanitize.Accents(strings.ToLower(playlist.Name)), sanitize.Accents(strings.ToLower(query)), ) }) - results = append(results, &mediaprovider.SearchResult{ - Name: fmt.Sprintf("Create new playlist: %s", query), - Type: mediaprovider.ContentTypePlaylist, - }) + results = append(results, filteredPlaylists...) } - 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 } From 44f7eebad18d2862fb418c6174f4c4409a23a10a Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 23 May 2024 16:32:07 -0700 Subject: [PATCH 09/14] customize placeholder text and fix update bug --- ui/dialogs/searchdialog.go | 11 ++++++++++- ui/dialogs/selectplaylist.go | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/ui/dialogs/searchdialog.go b/ui/dialogs/searchdialog.go index cfdadee..e63b25b 100644 --- a/ui/dialogs/searchdialog.go +++ b/ui/dialogs/searchdialog.go @@ -24,6 +24,8 @@ import ( type SearchDialog struct { widget.BaseWidget + PlaceholderText string + OnDismiss func() OnNavigateTo func(mediaprovider.ContentType, string) OnSearched func(string) []*mediaprovider.SearchResult @@ -97,6 +99,13 @@ func (sd *SearchDialog) Show() { sd.BaseWidget.Show() } +func (sd *SearchDialog) Refresh() { + if sd.PlaceholderText != "" { + sd.searchEntry.SetPlaceHolder(sd.PlaceholderText) + } + sd.BaseWidget.Refresh() +} + func (sd *SearchDialog) onDismiss() { if sd.OnDismiss != nil { sd.OnDismiss() @@ -267,7 +276,7 @@ func (s *searchResult) Update(result *mediaprovider.SearchResult) { if result == nil { return } - if s.contentType == result.Type && s.id == result.ID { + if s.contentType == result.Type && s.id == result.ID && s.title.Text == result.Name { return // nothing to do } s.id = result.ID diff --git a/ui/dialogs/selectplaylist.go b/ui/dialogs/selectplaylist.go index 53f2a86..46a4c80 100644 --- a/ui/dialogs/selectplaylist.go +++ b/ui/dialogs/selectplaylist.go @@ -35,6 +35,7 @@ func NewSelectPlaylistDialog(mp mediaprovider.MediaProvider, im util.ImageFetche sp.onSearched, sp.onInit, ) + sd.PlaceholderText = "Search playlists or new playlist name" sp.SearchDialog = sd return sp } From e2a9ea49e0f8ca76351df5469d8ddb8c29a9a9b5 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 23 May 2024 16:35:37 -0700 Subject: [PATCH 10/14] rename skip duplicates check --- ui/dialogs/selectplaylist.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/dialogs/selectplaylist.go b/ui/dialogs/selectplaylist.go index 46a4c80..3e002eb 100644 --- a/ui/dialogs/selectplaylist.go +++ b/ui/dialogs/selectplaylist.go @@ -52,7 +52,7 @@ func (sp *SelectPlaylist) onInit() ([]*mediaprovider.SearchResult, *widget.Check return playlist.Owner == sp.loggedInUser }) sp.allPlaylistResuts = sharedutil.MapSlice(userPlaylists, sp.playlistToSearchResult) - skipDuplicatesCheck := widget.NewCheck("Skip duplicates", func(checked bool) { + skipDuplicatesCheck := widget.NewCheck("Skip duplicate tracks", func(checked bool) { sp.SkipDuplicates = checked }) return results, skipDuplicatesCheck From 7a12f58658149451b55d9c65eb3c43da57880c15 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 23 May 2024 17:29:32 -0700 Subject: [PATCH 11/14] use map to de-duplicate playlist tracks --- ui/controller/controller.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/ui/controller/controller.go b/ui/controller/controller.go index 9c4aea0..95d149c 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -11,7 +11,6 @@ import ( "net/url" "os" "path/filepath" - "slices" "time" "github.com/dweymouth/supersonic/backend" @@ -340,22 +339,20 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { } else { m.App.Config.Application.DefaultPlaylistID = id if sp.SkipDuplicates { - var filterTrackIDs []string go func() { + currentTrackIDs := make(map[string]struct{}) if selectedPlaylist, err := m.App.ServerManager.Server.GetPlaylist(id); err != nil { log.Printf("error getting playlist: %s", err.Error()) } else { - var trackIDsInPaylist []string - for _, track := range selectedPlaylist.Tracks { - trackIDsInPaylist = append(trackIDsInPaylist, track.ID) + currentTrackIDs[track.ID] = struct{}{} } - filterTrackIDs = sharedutil.FilterSlice(trackIDs, func(trackID string) bool { - return !slices.Contains(trackIDsInPaylist, trackID) + filterTrackIDs := sharedutil.FilterSlice(trackIDs, func(trackID string) bool { + _, ok := currentTrackIDs[trackID] + return !ok }) - + m.App.ServerManager.Server.AddPlaylistTracks(id, filterTrackIDs) } - m.App.ServerManager.Server.AddPlaylistTracks(id, filterTrackIDs) }() } else { go m.App.ServerManager.Server.AddPlaylistTracks(id, trackIDs) From 3c5d1d521b8f7a6dc700880dca486ed5cb2cd0d0 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 24 May 2024 07:37:34 -0700 Subject: [PATCH 12/14] remove old addtoplaylistdialog --- ui/dialogs/addtoplaylistdialog.go | 120 ------------------------------ 1 file changed, 120 deletions(-) delete mode 100644 ui/dialogs/addtoplaylistdialog.go diff --git a/ui/dialogs/addtoplaylistdialog.go b/ui/dialogs/addtoplaylistdialog.go deleted file mode 100644 index 0f8e16f..0000000 --- a/ui/dialogs/addtoplaylistdialog.go +++ /dev/null @@ -1,120 +0,0 @@ -package dialogs - -import ( - "time" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/layout" - "fyne.io/fyne/v2/widget" -) - -type AddToPlaylistDialog struct { - widget.BaseWidget - - OnCanceled func() - OnSubmit func(playlistChoice int, newPlaylistName string) - - playlistSelect *widget.Select - newPlaylistLabel *widget.Label - newPlaylistName *widget.Entry - okBtn *widget.Button - - container *fyne.Container -} - -var _ fyne.Widget = (*AddToPlaylistDialog)(nil) - -func NewAddToPlaylistDialog(title string, existingPlaylistNames []string, selectedIdx int) *AddToPlaylistDialog { - a := &AddToPlaylistDialog{} - a.ExtendBaseWidget(a) - - titleLabel := widget.NewLabel(title) - titleLabel.TextStyle.Bold = true - options := []string{"New playlist..."} - options = append(options, existingPlaylistNames...) - a.playlistSelect = widget.NewSelect(options, func(_ string) { - a.onSelectionChanged() - }) - a.playlistSelect.PlaceHolder = "(Choose playlist)" - if selectedIdx >= 0 { - // calling SetSelectedIndex before showing the Select crashes... - go func() { - time.Sleep(10 * time.Millisecond) - // add 1 to selectedIdx to account for "(Choose playlist)" entry - a.playlistSelect.SetSelectedIndex(selectedIdx + 1) - }() - } - a.newPlaylistName = widget.NewEntry() - a.newPlaylistName.Hidden = true - a.newPlaylistName.OnChanged = func(text string) { - if len(text) > 0 { - a.okBtn.Enable() - } else { - a.okBtn.Disable() - } - } - a.newPlaylistLabel = widget.NewLabel("Name") - a.newPlaylistLabel.Hidden = true - - a.okBtn = widget.NewButton("OK", a.onOK) - a.okBtn.Importance = widget.HighImportance - a.okBtn.Disable() - cancelBtn := widget.NewButton("Cancel", a.onCancel) - - a.container = container.NewVBox( - container.NewHBox(layout.NewSpacer(), titleLabel, layout.NewSpacer()), - container.New(layout.NewFormLayout(), - widget.NewLabel("Playlist"), - a.playlistSelect, - a.newPlaylistLabel, - a.newPlaylistName), - widget.NewSeparator(), - container.NewHBox(layout.NewSpacer(), a.okBtn, cancelBtn)) - - return a -} - -func (a *AddToPlaylistDialog) onOK() { - var newPlaylistName string - playlistChoice := -1 - if sel := a.playlistSelect.SelectedIndex(); sel == 0 { - newPlaylistName = a.newPlaylistName.Text - } else { - playlistChoice = sel - 1 - } - if a.OnSubmit != nil { - a.OnSubmit(playlistChoice, newPlaylistName) - } -} - -func (a *AddToPlaylistDialog) onSelectionChanged() { - if a.playlistSelect.SelectedIndex() == 0 { - a.newPlaylistName.Show() - a.newPlaylistLabel.Show() - if len(a.newPlaylistName.Text) == 0 { - a.okBtn.Disable() - } else { - a.okBtn.Enable() - } - } else { - a.newPlaylistName.Hide() - a.newPlaylistLabel.Hide() - a.okBtn.Enable() - } -} - -func (a *AddToPlaylistDialog) onCancel() { - if a.OnCanceled != nil { - a.OnCanceled() - } -} - -func (a *AddToPlaylistDialog) MinSize() fyne.Size { - a.ExtendBaseWidget(a) - return fyne.NewSize(300, a.container.MinSize().Height) -} - -func (a *AddToPlaylistDialog) CreateRenderer() fyne.WidgetRenderer { - return widget.NewSimpleRenderer(a.container) -} From 448421e556ee8e3fa618056a61b458c96ff05690 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 24 May 2024 07:53:34 -0700 Subject: [PATCH 13/14] simplify init of searchdialog --- ui/dialogs/quicksearch.go | 14 +----- ui/dialogs/searchdialog.go | 88 ++++++++++++++---------------------- ui/dialogs/selectplaylist.go | 16 +++---- 3 files changed, 42 insertions(+), 76 deletions(-) diff --git a/ui/dialogs/quicksearch.go b/ui/dialogs/quicksearch.go index f75fc6d..13d2381 100644 --- a/ui/dialogs/quicksearch.go +++ b/ui/dialogs/quicksearch.go @@ -14,18 +14,8 @@ type QuickSearch struct { } func NewQuickSearch(mp mediaprovider.MediaProvider, im util.ImageFetcher) *QuickSearch { - - q := &QuickSearch{ - mp: mp, - } - - sd := NewSearchDialog( - im, - "Quick Search", - q.onSearched, - nil, - ) - q.SearchDialog = sd + q := &QuickSearch{mp: mp} + q.SearchDialog = NewSearchDialog(im, "Quick Search", "Close", q.onSearched) return q } diff --git a/ui/dialogs/searchdialog.go b/ui/dialogs/searchdialog.go index e63b25b..8f646e1 100644 --- a/ui/dialogs/searchdialog.go +++ b/ui/dialogs/searchdialog.go @@ -26,30 +26,34 @@ type SearchDialog struct { PlaceholderText string + // Additional item that can be placed to the left + // of the dismiss buttons + ActionItem fyne.CanvasObject + OnDismiss func() OnNavigateTo func(mediaprovider.ContentType, string) OnSearched func(string) []*mediaprovider.SearchResult - OnInit func() ([]*mediaprovider.SearchResult, *widget.Check) imgSource util.ImageFetcher resultsMutex sync.RWMutex searchResults []*mediaprovider.SearchResult selectedIndex int - searchEntry *searchEntry - loadingDots *widgets.LoadingDots - list *widget.List - placeholderTitle string - content *fyne.Container + searchEntry *searchEntry + loadingDots *widgets.LoadingDots + list *widget.List + dialogTitle string + dismissText string + content *fyne.Container } -func NewSearchDialog(im util.ImageFetcher, placeholderTitle string, onSearched func(string) []*mediaprovider.SearchResult, onInit func() ([]*mediaprovider.SearchResult, *widget.Check)) *SearchDialog { +func NewSearchDialog(im util.ImageFetcher, title, dismissBtn string, onSearched func(string) []*mediaprovider.SearchResult) *SearchDialog { sd := &SearchDialog{ - imgSource: im, - loadingDots: widgets.NewLoadingDots(), - OnSearched: onSearched, - OnInit: onInit, - placeholderTitle: placeholderTitle, + imgSource: im, + loadingDots: widgets.NewLoadingDots(), + OnSearched: onSearched, + dialogTitle: title, + dismissText: dismissBtn, } sd.ExtendBaseWidget(sd) @@ -95,8 +99,8 @@ func (sd *SearchDialog) SearchQuery() string { } func (sd *SearchDialog) Show() { - sd.onInit() sd.BaseWidget.Show() + go sd.onSearched("") } func (sd *SearchDialog) Refresh() { @@ -155,47 +159,6 @@ func (sd *SearchDialog) setResults(results []*mediaprovider.SearchResult) { sd.list.Select(0) } -func (sd *SearchDialog) SetContent(checkBox *widget.Check) { - dismissBtn := widget.NewButton("Close", sd.onDismiss) - title := widget.NewRichText(&widget.TextSegment{Text: sd.placeholderTitle, Style: util.BoldRichTextStyle}) - title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter - if checkBox != nil { - sd.content = container.NewStack( - container.NewBorder( - container.NewVBox(title, sd.searchEntry), - container.NewVBox(widget.NewSeparator(), container.NewHBox(checkBox, layout.NewSpacer(), dismissBtn)), - nil, nil, sd.list), - container.NewCenter(sd.loadingDots), - ) - } else { - sd.content = container.NewStack( - container.NewBorder( - container.NewVBox(title, sd.searchEntry), - container.NewVBox(widget.NewSeparator(), container.NewHBox(layout.NewSpacer(), dismissBtn)), - nil, nil, sd.list), - container.NewCenter(sd.loadingDots), - ) - } -} - -func (sd *SearchDialog) onInit() { - if sd.OnInit == nil { - sd.SetContent(nil) - return - } - sd.loadingDots.Start() - var results []*mediaprovider.SearchResult - res, checkBox := sd.OnInit() - if len(res) == 0 { - log.Println("No results") - } else { - results = res - } - sd.SetContent(checkBox) - sd.loadingDots.Stop() - sd.setResults(results) -} - func (sd *SearchDialog) onSearched(query string) { sd.loadingDots.Start() var results []*mediaprovider.SearchResult @@ -210,6 +173,23 @@ func (sd *SearchDialog) onSearched(query string) { } func (sd *SearchDialog) CreateRenderer() fyne.WidgetRenderer { + dismissBtn := widget.NewButton(sd.dismissText, sd.onDismiss) + title := widget.NewRichText(&widget.TextSegment{Text: sd.dialogTitle, Style: util.BoldRichTextStyle}) + title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter + bottomRow := container.NewHBox() + if sd.ActionItem != nil { + bottomRow.Objects = []fyne.CanvasObject{sd.ActionItem, layout.NewSpacer(), dismissBtn} + } else { + bottomRow.Objects = []fyne.CanvasObject{layout.NewSpacer(), dismissBtn} + } + sd.content = container.NewStack( + container.NewBorder( + container.NewVBox(title, sd.searchEntry), + container.NewVBox(widget.NewSeparator(), bottomRow), + nil, nil, sd.list), + container.NewCenter(sd.loadingDots), + ) + return widget.NewSimpleRenderer(sd.content) } diff --git a/ui/dialogs/selectplaylist.go b/ui/dialogs/selectplaylist.go index 3e002eb..d3c6fcf 100644 --- a/ui/dialogs/selectplaylist.go +++ b/ui/dialogs/selectplaylist.go @@ -7,6 +7,7 @@ import ( "strings" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/widget" "github.com/deluan/sanitize" "github.com/dweymouth/supersonic/backend/mediaprovider" @@ -23,39 +24,34 @@ type SelectPlaylist struct { } func NewSelectPlaylistDialog(mp mediaprovider.MediaProvider, im util.ImageFetcher, loggedInUser string) *SelectPlaylist { - sp := &SelectPlaylist{ mp: mp, loggedInUser: loggedInUser, SkipDuplicates: false, } + sp.fetchUserOwnedPlaylists() sd := NewSearchDialog( im, - "Select playlist", + "Add to playlist", + "Cancel", sp.onSearched, - sp.onInit, ) + sd.ActionItem = widget.NewCheckWithData("Skip duplicate tracks", binding.BindBool(&sp.SkipDuplicates)) sd.PlaceholderText = "Search playlists or new playlist name" sp.SearchDialog = sd return sp } -func (sp *SelectPlaylist) onInit() ([]*mediaprovider.SearchResult, *widget.Check) { - var results []*mediaprovider.SearchResult +func (sp *SelectPlaylist) fetchUserOwnedPlaylists() { playlists, err := sp.mp.GetPlaylists() if err != nil { // TODO: surface this error to user log.Printf("error getting playlists: %s", err.Error()) - return results, nil } userPlaylists := sharedutil.FilterSlice(playlists, func(playlist *mediaprovider.Playlist) bool { return playlist.Owner == sp.loggedInUser }) sp.allPlaylistResuts = sharedutil.MapSlice(userPlaylists, sp.playlistToSearchResult) - skipDuplicatesCheck := widget.NewCheck("Skip duplicate tracks", func(checked bool) { - sp.SkipDuplicates = checked - }) - return results, skipDuplicatesCheck } func (sp *SelectPlaylist) playlistToSearchResult(playlist *mediaprovider.Playlist) *mediaprovider.SearchResult { From 34427e5033a7cda743682909a20b9dd8994bc470 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 24 May 2024 07:58:40 -0700 Subject: [PATCH 14/14] move "Create new playlist" result to bottom of list --- ui/dialogs/selectplaylist.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ui/dialogs/selectplaylist.go b/ui/dialogs/selectplaylist.go index d3c6fcf..9032e0d 100644 --- a/ui/dialogs/selectplaylist.go +++ b/ui/dialogs/selectplaylist.go @@ -29,7 +29,6 @@ func NewSelectPlaylistDialog(mp mediaprovider.MediaProvider, im util.ImageFetche loggedInUser: loggedInUser, SkipDuplicates: false, } - sp.fetchUserOwnedPlaylists() sd := NewSearchDialog( im, "Add to playlist", @@ -69,21 +68,23 @@ func (sp *SelectPlaylist) playlistToSearchResult(playlist *mediaprovider.Playlis } func (sp *SelectPlaylist) onSearched(query string) []*mediaprovider.SearchResult { + if sp.allPlaylistResuts == nil { + sp.fetchUserOwnedPlaylists() + } var results []*mediaprovider.SearchResult if query == "" { results = sp.allPlaylistResuts } else { - results = []*mediaprovider.SearchResult{{ - Name: fmt.Sprintf("Create new playlist: %s", query), - Type: mediaprovider.ContentTypePlaylist, - }} - filteredPlaylists := sharedutil.FilterSlice(sp.allPlaylistResuts, func(playlist *mediaprovider.SearchResult) bool { + results = sharedutil.FilterSlice(sp.allPlaylistResuts, func(playlist *mediaprovider.SearchResult) bool { return strings.Contains( sanitize.Accents(strings.ToLower(playlist.Name)), sanitize.Accents(strings.ToLower(query)), ) }) - results = append(results, filteredPlaylists...) + results = append(results, &mediaprovider.SearchResult{ + Name: fmt.Sprintf("Create new playlist: %s", query), + Type: mediaprovider.ContentTypePlaylist, + }) } return results