From 14e03bed0cbc40fa6b7017fa27a16a96e28b641c Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 5 Mar 2025 16:55:52 -0800 Subject: [PATCH 1/3] initial work for playlist search --- ui/browsing/playlistpage.go | 45 ++++++++++++++++++++++++++++++++++++- ui/widgets/searchentry.go | 21 +++++++++++++++-- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 01caba2..2098840 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -5,6 +5,7 @@ import ( "log" "strings" + ttwidget "github.com/dweymouth/fyne-tooltip/widget" "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/sharedutil" @@ -14,6 +15,7 @@ import ( "github.com/dweymouth/supersonic/ui/widgets" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/lang" "fyne.io/fyne/v2/layout" @@ -275,6 +277,9 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { a.ownerLabel = util.NewTruncatingLabel() a.createdAtLabel = widget.NewLabel("") a.trackTimeLabel = widget.NewLabel("") + + var buttonRow *fyne.Container + a.editButton = widget.NewButtonWithIcon(lang.L("Edit"), theme.DocumentCreateIcon(), func() { if a.playlistInfo != nil { a.page.contr.DoEditPlaylistWorkflow(&a.playlistInfo.Playlist) @@ -289,6 +294,42 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { a.page.pm.LoadTracks(a.page.tracks, backend.Replace, true) a.page.pm.PlayFromBeginning() }) + var searchBtn *ttwidget.Button + var searchEntry *widgets.SearchEntry + searchBtn = ttwidget.NewButtonWithIcon("", theme.SearchIcon(), func() { + minW := searchBtn.MinSize().Width + if searchEntry == nil { + searchEntry = widgets.NewSearchEntry() + searchEntry.PlaceHolder = "" + searchEntry.Scroll = container.ScrollNone + searchEntry.OnFocusLost = func() { + if searchEntry.Text == "" { + searchEntry.SetPlaceHolder("") + fyne.NewAnimation(canvas.DurationShort, func(f float32) { + f = 1 - f + w := (200-minW)*f + minW + searchEntry.SetMinWidth(w) + if f == 0 { + buttonRow.Objects[3] = searchBtn + } + buttonRow.Layout.Layout(buttonRow.Objects, buttonRow.Layout.MinSize(buttonRow.Objects)) + }).Start() + } + } + } + buttonRow.Objects[3] = searchEntry + searchEntry.SetMinWidth(minW) + fyne.CurrentApp().Driver().CanvasForObject(a).Focus(searchEntry) + fyne.NewAnimation(canvas.DurationShort, func(f float32) { + w := (200-minW)*f + minW + searchEntry.SetMinWidth(w) + buttonRow.Layout.Layout(buttonRow.Objects, buttonRow.Layout.MinSize(buttonRow.Objects)) + if f == 1 { + searchEntry.SetPlaceHolder(lang.L("Search")) + } + }).Start() + }) + searchBtn.SetToolTip(lang.L("Search")) var pop *widget.PopUpMenu menuBtn := widget.NewButtonWithIcon("", theme.MoreHorizontalIcon(), nil) menuBtn.OnTapped = func() { @@ -317,13 +358,15 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { pop.ShowAtPosition(fyne.NewPos(pos.X, pos.Y+menuBtn.Size().Height)) } + buttonRow = container.NewHBox(a.editButton, playButton, shuffleBtn, searchBtn, menuBtn) + a.container = util.AddHeaderBackground( container.NewBorder(nil, nil, a.image, nil, container.NewVBox(a.titleLabel, container.New(layout.NewCustomPaddedVBoxLayout(theme.Padding()-10), a.descriptionLabel, a.ownerLabel, a.trackTimeLabel), - container.NewHBox(a.editButton, playButton, shuffleBtn, menuBtn), + buttonRow, ))) return a } diff --git a/ui/widgets/searchentry.go b/ui/widgets/searchentry.go index f998bc1..c71e4da 100644 --- a/ui/widgets/searchentry.go +++ b/ui/widgets/searchentry.go @@ -17,7 +17,9 @@ import ( type SearchEntry struct { widget.Entry - OnSearched func(string) + minWidth float32 + OnSearched func(string) + OnFocusLost func() } func NewSearchEntry() *SearchEntry { @@ -53,13 +55,28 @@ func (s *SearchEntry) TypedKey(e *fyne.KeyEvent) { s.Entry.TypedKey(e) } +func (s *SearchEntry) FocusLost() { + s.Entry.FocusLost() + if s.OnFocusLost != nil { + s.OnFocusLost() + } +} + func (s *SearchEntry) Refresh() { s.updateActionButton() s.Entry.Refresh() } +func (s *SearchEntry) SetMinWidth(w float32) { + s.minWidth = w +} + func (s *SearchEntry) MinSize() fyne.Size { - return fyne.NewSize(200, s.Entry.MinSize().Height) + w := s.minWidth + if w == 0 { + w = 200 + } + return fyne.NewSize(w, s.Entry.MinSize().Height) } func (s *SearchEntry) updateActionButton() bool { From 15b73893c3cb6a016fa316ae09b79cebb5c61369 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 6 Mar 2025 19:24:21 -0800 Subject: [PATCH 2/3] implement search within playlist --- ui/browsing/playlistpage.go | 46 +++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 2098840..2f12ac5 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -5,6 +5,7 @@ import ( "log" "strings" + "github.com/deluan/sanitize" ttwidget "github.com/dweymouth/fyne-tooltip/widget" "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" @@ -242,6 +243,37 @@ func (a *PlaylistPage) onRemoveSelectedFromPlaylist() { a.Reload() } +func (a *PlaylistPage) onSearched(query string) { + if query == "" { + a.tracklist.Options.Reorderable = true + ids := a.tracklist.SelectedTrackIDs() + a.tracklist.SetTracks(a.tracks) + // if a track was selected in the searched view, + // scroll to it when switching back to full playlist + if len(ids) > 0 { + a.tracklist.SelectAndScrollToTrack(ids[0]) + } + return + } + + searched := sharedutil.FilterSlice(a.tracks, func(t *mediaprovider.Track) bool { + sani := func(s string) string { + return strings.ToLower(sanitize.Accents(s)) + } + qLower := strings.ToLower(query) + return strings.Contains(sani(t.Title), qLower) || + strings.Contains(sani(strings.Join(t.ArtistNames, "")), qLower) || + strings.Contains(sani(t.Album), qLower) || + strings.Contains(sani(strings.Join(t.Genres, "")), qLower) || + strings.Contains(sani(strings.Join(t.ComposerNames, "")), qLower) || + strings.Contains(fmt.Sprintf("%d", t.Year), query) || + strings.Contains(sani(t.Comment), qLower) + }) + + a.tracklist.Options.Reorderable = false + a.tracklist.SetTracks(searched) +} + type PlaylistPageHeader struct { widget.BaseWidget @@ -278,8 +310,6 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { a.createdAtLabel = widget.NewLabel("") a.trackTimeLabel = widget.NewLabel("") - var buttonRow *fyne.Container - a.editButton = widget.NewButtonWithIcon(lang.L("Edit"), theme.DocumentCreateIcon(), func() { if a.playlistInfo != nil { a.page.contr.DoEditPlaylistWorkflow(&a.playlistInfo.Playlist) @@ -294,6 +324,10 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { a.page.pm.LoadTracks(a.page.tracks, backend.Replace, true) a.page.pm.PlayFromBeginning() }) + + // set up search button / entry animation pair + // needs reference to containing HBox container (initialized later) + var buttonRow *fyne.Container var searchBtn *ttwidget.Button var searchEntry *widgets.SearchEntry searchBtn = ttwidget.NewButtonWithIcon("", theme.SearchIcon(), func() { @@ -302,8 +336,11 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { searchEntry = widgets.NewSearchEntry() searchEntry.PlaceHolder = "" searchEntry.Scroll = container.ScrollNone + searchEntry.OnSearched = page.onSearched searchEntry.OnFocusLost = func() { if searchEntry.Text == "" { + // dismissal animation + searchEntry.Scroll = container.ScrollNone searchEntry.SetPlaceHolder("") fyne.NewAnimation(canvas.DurationShort, func(f float32) { f = 1 - f @@ -312,6 +349,7 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { if f == 0 { buttonRow.Objects[3] = searchBtn } + // re-layout container (without unneeded full refresh) buttonRow.Layout.Layout(buttonRow.Objects, buttonRow.Layout.MinSize(buttonRow.Objects)) }).Start() } @@ -323,13 +361,17 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { fyne.NewAnimation(canvas.DurationShort, func(f float32) { w := (200-minW)*f + minW searchEntry.SetMinWidth(w) + // re-layout container (without unneeded full refresh) buttonRow.Layout.Layout(buttonRow.Objects, buttonRow.Layout.MinSize(buttonRow.Objects)) if f == 1 { + searchEntry.Scroll = container.ScrollHorizontalOnly + searchEntry.Refresh() // needed to initialize widget's scroller searchEntry.SetPlaceHolder(lang.L("Search")) } }).Start() }) searchBtn.SetToolTip(lang.L("Search")) + var pop *widget.PopUpMenu menuBtn := widget.NewButtonWithIcon("", theme.MoreHorizontalIcon(), nil) menuBtn.OnTapped = func() { From 56c32e529ee1d498e7532d480020c6f64429f75c Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 7 Mar 2025 08:04:15 -0800 Subject: [PATCH 3/3] fix a few bugs --- res/translations/en.json | 2 +- ui/browsing/playlistpage.go | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/res/translations/en.json b/res/translations/en.json index fabadfc..5c10897 100644 --- a/res/translations/en.json +++ b/res/translations/en.json @@ -168,6 +168,7 @@ "Save play queue on exit": "Save play queue on exit", "Saved at": "Saved at", "Scrobble when": "Scrobble when", + "Search": "Search", "Search Everywhere": "Search Everywhere", "Search page": "Search page", "Search playlists or new playlist name": "Search playlists or new playlist name", @@ -237,7 +238,6 @@ "You are running the latest version of": "You are running the latest version of", "Quit": "Quit", "Rescan Library": "Rescan Library", - "Search": "Search", "Owner": "Owner", "To server": "To server", "Fav.": "Fav.", diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 2f12ac5..6319025 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -245,8 +245,11 @@ func (a *PlaylistPage) onRemoveSelectedFromPlaylist() { func (a *PlaylistPage) onSearched(query string) { if query == "" { - a.tracklist.Options.Reorderable = true + // switch back to full playlist view + ids := a.tracklist.SelectedTrackIDs() + a.tracklist.Options.Reorderable = true + a.tracklist.SetSorting(a.trackSort) // restore old sort order a.tracklist.SetTracks(a.tracks) // if a track was selected in the searched view, // scroll to it when switching back to full playlist @@ -256,6 +259,7 @@ func (a *PlaylistPage) onSearched(query string) { return } + // search tracks within the playlist searched := sharedutil.FilterSlice(a.tracks, func(t *mediaprovider.Track) bool { sani := func(s string) string { return strings.ToLower(sanitize.Accents(s)) @@ -270,7 +274,9 @@ func (a *PlaylistPage) onSearched(query string) { strings.Contains(sani(t.Comment), qLower) }) + a.trackSort = a.tracklist.Sorting() // save old sort order a.tracklist.Options.Reorderable = false + a.tracklist.SetSorting(widgets.TracklistSort{}) a.tracklist.SetTracks(searched) } @@ -336,7 +342,11 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { searchEntry = widgets.NewSearchEntry() searchEntry.PlaceHolder = "" searchEntry.Scroll = container.ScrollNone - searchEntry.OnSearched = page.onSearched + searchEntry.OnSearched = func(s string) { + // we can't assign a.page.onSearched directly, + // since widget reuse means a.page could change + a.page.onSearched(s) + } searchEntry.OnFocusLost = func() { if searchEntry.Text == "" { // dismissal animation