Merge pull request #574 from dweymouth/feat/search-playlists
Add ability to search tracks within a playlist
This commit is contained in:
@@ -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.",
|
||||
|
||||
@@ -5,6 +5,8 @@ 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"
|
||||
"github.com/dweymouth/supersonic/sharedutil"
|
||||
@@ -14,6 +16,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"
|
||||
@@ -240,6 +243,43 @@ func (a *PlaylistPage) onRemoveSelectedFromPlaylist() {
|
||||
a.Reload()
|
||||
}
|
||||
|
||||
func (a *PlaylistPage) onSearched(query string) {
|
||||
if query == "" {
|
||||
// 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
|
||||
if len(ids) > 0 {
|
||||
a.tracklist.SelectAndScrollToTrack(ids[0])
|
||||
}
|
||||
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))
|
||||
}
|
||||
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.trackSort = a.tracklist.Sorting() // save old sort order
|
||||
a.tracklist.Options.Reorderable = false
|
||||
a.tracklist.SetSorting(widgets.TracklistSort{})
|
||||
a.tracklist.SetTracks(searched)
|
||||
}
|
||||
|
||||
type PlaylistPageHeader struct {
|
||||
widget.BaseWidget
|
||||
|
||||
@@ -275,6 +315,7 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader {
|
||||
a.ownerLabel = util.NewTruncatingLabel()
|
||||
a.createdAtLabel = widget.NewLabel("")
|
||||
a.trackTimeLabel = widget.NewLabel("")
|
||||
|
||||
a.editButton = widget.NewButtonWithIcon(lang.L("Edit"), theme.DocumentCreateIcon(), func() {
|
||||
if a.playlistInfo != nil {
|
||||
a.page.contr.DoEditPlaylistWorkflow(&a.playlistInfo.Playlist)
|
||||
@@ -289,6 +330,58 @@ 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() {
|
||||
minW := searchBtn.MinSize().Width
|
||||
if searchEntry == nil {
|
||||
searchEntry = widgets.NewSearchEntry()
|
||||
searchEntry.PlaceHolder = ""
|
||||
searchEntry.Scroll = container.ScrollNone
|
||||
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
|
||||
searchEntry.Scroll = container.ScrollNone
|
||||
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
|
||||
}
|
||||
// re-layout container (without unneeded full refresh)
|
||||
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)
|
||||
// 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() {
|
||||
@@ -317,13 +410,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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user