Fix #115: add search to artists, genres, and playlists pages

This commit is contained in:
Drew Weymouth
2023-04-10 17:38:33 -07:00
parent f95632a43c
commit 19a45b7361
4 changed files with 107 additions and 20 deletions
+1
View File
@@ -7,6 +7,7 @@
- [#119](https://github.com/dweymouth/supersonic/issues/119) Add file path column to tracklist view - [#119](https://github.com/dweymouth/supersonic/issues/119) Add file path column to tracklist view
- [#117](https://github.com/dweymouth/supersonic/issues/117) Add (optional) system tray menu and close to tray support - [#117](https://github.com/dweymouth/supersonic/issues/117) Add (optional) system tray menu and close to tray support
- [#55](https://github.com/dweymouth/supersonic/issues/55) Show disc number and disc count for multi-disc albums - [#55](https://github.com/dweymouth/supersonic/issues/55) Show disc number and disc count for multi-disc albums
- [#115](https://github.com/dweymouth/supersonic/issues/115) Add search bar to artist, genres, and playlists pages
### Fixes ### Fixes
- **todo-commithash** Don't show update available prompt if the found version is the same as the running app version - **todo-commithash** Don't show update available prompt if the found version is the same as the running app version
+10
View File
@@ -25,6 +25,16 @@ func IntSliceContains(slice []int, i int) bool {
return false return false
} }
func FilterSlice[T any](ss []T, test func(T) bool) []T {
result := make([]T, 0)
for _, s := range ss {
if test(s) {
result = append(result, s)
}
}
return result
}
func FindTrackByID(id string, tracks []*subsonic.Child) *subsonic.Child { func FindTrackByID(id string, tracks []*subsonic.Child) *subsonic.Child {
for _, tr := range tracks { for _, tr := range tracks {
if id == tr.ID { if id == tr.ID {
+46 -10
View File
@@ -2,7 +2,9 @@ package browsing
import ( import (
"log" "log"
"strings"
"supersonic/backend" "supersonic/backend"
"supersonic/sharedutil"
"supersonic/ui/controller" "supersonic/ui/controller"
"supersonic/ui/layouts" "supersonic/ui/layouts"
"supersonic/ui/widgets" "supersonic/ui/widgets"
@@ -10,6 +12,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic" "github.com/dweymouth/go-subsonic/subsonic"
@@ -23,12 +26,19 @@ type ArtistsGenresPage struct {
isGenresPage bool isGenresPage bool
contr *controller.Controller contr *controller.Controller
sm *backend.ServerManager sm *backend.ServerManager
titleDisp *widget.RichText model []widgets.ArtistGenreListItemModel
container *fyne.Container
list *widgets.ArtistGenreList list *widgets.ArtistGenreList
titleDisp *widget.RichText
container *fyne.Container
searcher *widgets.Searcher
} }
func NewArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *backend.ServerManager) *ArtistsGenresPage { func NewArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *backend.ServerManager) *ArtistsGenresPage {
return newArtistsGenresPage(isGenresPage, contr, sm, "")
}
func newArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *backend.ServerManager, searchText string) *ArtistsGenresPage {
title := "Artists" title := "Artists"
if isGenresPage { if isGenresPage {
title = "Genres" title = "Genres"
@@ -51,27 +61,49 @@ func NewArtistsGenresPage(isGenresPage bool, contr *controller.Controller, sm *b
a.contr.NavigateTo(controller.ArtistRoute(id)) a.contr.NavigateTo(controller.ArtistRoute(id))
} }
} }
a.searcher = widgets.NewSearcher()
a.searcher.OnSearched = a.onSearched
a.searcher.Entry.Text = searchText
a.buildContainer() a.buildContainer()
go a.load() go a.load(searchText != "")
return a return a
} }
// should be called asynchronously // should be called asynchronously
func (a *ArtistsGenresPage) load() { func (a *ArtistsGenresPage) load(searchOnLoad bool) {
if a.isGenresPage { if a.isGenresPage {
genres, err := a.sm.Server.GetGenres() genres, err := a.sm.Server.GetGenres()
if err != nil { if err != nil {
log.Printf("error loading genres: %v", err.Error()) log.Printf("error loading genres: %v", err.Error())
} }
a.list.Items = a.buildGenresListModel(genres) a.model = a.buildGenresListModel(genres)
} else { } else {
artists, err := a.sm.Server.GetArtists(nil) artists, err := a.sm.Server.GetArtists(nil)
if err != nil { if err != nil {
log.Printf("error loading artists: %v", err.Error()) log.Printf("error loading artists: %v", err.Error())
} }
a.list.Items = a.buildArtistListModel(artists) a.model = a.buildArtistListModel(artists)
} }
a.Refresh() if searchOnLoad {
a.onSearched(a.searcher.Entry.Text)
} else {
a.list.Items = a.model
a.list.Refresh()
}
}
func (a *ArtistsGenresPage) onSearched(query string) {
// since the artists and genres lists are returned in full non-paginated, we will do our own
// simple search based on the artist/genre name, rather than calling a server API
if query == "" {
a.list.Items = a.model
} else {
result := sharedutil.FilterSlice(a.model, func(x widgets.ArtistGenreListItemModel) bool {
return strings.Contains(strings.ToLower(x.Name), strings.ToLower(query))
})
a.list.Items = result
}
a.list.Refresh()
} }
func (a *ArtistsGenresPage) Route() controller.Route { func (a *ArtistsGenresPage) Route() controller.Route {
@@ -82,7 +114,7 @@ func (a *ArtistsGenresPage) Route() controller.Route {
} }
func (a *ArtistsGenresPage) Reload() { func (a *ArtistsGenresPage) Reload() {
go a.load() go a.load(false)
} }
func (a *ArtistsGenresPage) Save() SavedPage { func (a *ArtistsGenresPage) Save() SavedPage {
@@ -90,6 +122,7 @@ func (a *ArtistsGenresPage) Save() SavedPage {
isGenresPage: a.isGenresPage, isGenresPage: a.isGenresPage,
contr: a.contr, contr: a.contr,
sm: a.sm, sm: a.sm,
searchText: a.searcher.Entry.Text,
} }
} }
@@ -97,10 +130,11 @@ type savedArtistsGenresPage struct {
isGenresPage bool isGenresPage bool
contr *controller.Controller contr *controller.Controller
sm *backend.ServerManager sm *backend.ServerManager
searchText string
} }
func (s *savedArtistsGenresPage) Restore() Page { func (s *savedArtistsGenresPage) Restore() Page {
return NewArtistsGenresPage(s.isGenresPage, s.contr, s.sm) return newArtistsGenresPage(s.isGenresPage, s.contr, s.sm, s.searchText)
} }
func (a *ArtistsGenresPage) buildArtistListModel(artists *subsonic.ArtistsID3) []widgets.ArtistGenreListItemModel { func (a *ArtistsGenresPage) buildArtistListModel(artists *subsonic.ArtistsID3) []widgets.ArtistGenreListItemModel {
@@ -133,9 +167,11 @@ func (a *ArtistsGenresPage) buildGenresListModel(genres []*subsonic.Genre) []wid
} }
func (a *ArtistsGenresPage) buildContainer() { func (a *ArtistsGenresPage) buildContainer() {
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher.Entry, layout.NewSpacer())
a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15}, a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
container.NewBorder( container.NewBorder(
container.New(&layouts.MaxPadLayout{PadLeft: -5}, a.titleDisp), container.New(&layouts.MaxPadLayout{PadLeft: -5},
container.NewHBox(a.titleDisp, layout.NewSpacer(), searchVbox)),
nil, nil, nil, a.list)) nil, nil, nil, a.list))
} }
+50 -10
View File
@@ -3,13 +3,16 @@ package browsing
import ( import (
"log" "log"
"strconv" "strconv"
"strings"
"supersonic/backend" "supersonic/backend"
"supersonic/sharedutil"
"supersonic/ui/controller" "supersonic/ui/controller"
"supersonic/ui/layouts" "supersonic/ui/layouts"
"supersonic/ui/widgets" "supersonic/ui/widgets"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"github.com/dweymouth/go-subsonic/subsonic" "github.com/dweymouth/go-subsonic/subsonic"
@@ -20,12 +23,19 @@ type PlaylistsPage struct {
contr *controller.Controller contr *controller.Controller
sm *backend.ServerManager sm *backend.ServerManager
playlists []*subsonic.Playlist
searcher *widgets.Searcher
titleDisp *widget.RichText titleDisp *widget.RichText
container *fyne.Container container *fyne.Container
list *PlaylistList list *PlaylistList
} }
func NewPlaylistsPage(contr *controller.Controller, sm *backend.ServerManager) *PlaylistsPage { func NewPlaylistsPage(contr *controller.Controller, sm *backend.ServerManager) *PlaylistsPage {
return newPlaylistsPage(contr, sm, "")
}
func newPlaylistsPage(contr *controller.Controller, sm *backend.ServerManager, searchText string) *PlaylistsPage {
a := &PlaylistsPage{ a := &PlaylistsPage{
sm: sm, sm: sm,
contr: contr, contr: contr,
@@ -37,17 +47,42 @@ func NewPlaylistsPage(contr *controller.Controller, sm *backend.ServerManager) *
a.list.OnNavTo = func(id string) { a.list.OnNavTo = func(id string) {
a.contr.NavigateTo(controller.PlaylistRoute(id)) a.contr.NavigateTo(controller.PlaylistRoute(id))
} }
a.searcher = widgets.NewSearcher()
a.searcher.OnSearched = a.onSearched
a.searcher.Entry.Text = searchText
a.buildContainer() a.buildContainer()
go a.loadAsync() go a.load(searchText != "")
return a return a
} }
func (a *PlaylistsPage) loadAsync() { func (a *PlaylistsPage) load(searchOnLoad bool) {
playlists, err := a.sm.Server.GetPlaylists(nil) playlists, err := a.sm.Server.GetPlaylists(nil)
if err != nil { if err != nil {
log.Printf("error loading playlists: %v", err.Error()) log.Printf("error loading playlists: %v", err.Error())
} }
a.list.Playlists = playlists a.playlists = playlists
if searchOnLoad {
a.onSearched(a.searcher.Entry.Text)
} else {
a.list.Playlists = playlists
a.list.Refresh()
}
}
func (a *PlaylistsPage) onSearched(query string) {
// since the playlist list is returned in full non-paginated, we will do our own
// simple search based on the name, description, and owner, rather than calling a server API
if query == "" {
a.list.Playlists = a.playlists
} else {
result := sharedutil.FilterSlice(a.playlists, func(p *subsonic.Playlist) bool {
qLower := strings.ToLower(query)
return strings.Contains(strings.ToLower(p.Name), qLower) ||
strings.Contains(strings.ToLower(p.Comment), qLower) ||
strings.Contains(strings.ToLower(p.Owner), qLower)
})
a.list.Playlists = result
}
a.list.Refresh() a.list.Refresh()
} }
@@ -56,28 +91,33 @@ func (a *PlaylistsPage) Route() controller.Route {
} }
func (a *PlaylistsPage) Reload() { func (a *PlaylistsPage) Reload() {
go a.loadAsync() go a.load(false)
} }
func (a *PlaylistsPage) Save() SavedPage { func (a *PlaylistsPage) Save() SavedPage {
return &savedPlaylistsPage{ return &savedPlaylistsPage{
contr: a.contr, contr: a.contr,
sm: a.sm, sm: a.sm,
searchText: a.searcher.Entry.Text,
} }
} }
type savedPlaylistsPage struct { type savedPlaylistsPage struct {
contr *controller.Controller contr *controller.Controller
sm *backend.ServerManager sm *backend.ServerManager
searchText string
} }
func (s *savedPlaylistsPage) Restore() Page { func (s *savedPlaylistsPage) Restore() Page {
return NewPlaylistsPage(s.contr, s.sm) return newPlaylistsPage(s.contr, s.sm, s.searchText)
} }
func (a *PlaylistsPage) buildContainer() { func (a *PlaylistsPage) buildContainer() {
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher.Entry, layout.NewSpacer())
a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15}, a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15},
container.NewBorder(a.titleDisp, nil, nil, nil, a.list)) container.NewBorder(
container.NewHBox(a.titleDisp, layout.NewSpacer(), searchVbox),
nil, nil, nil, a.list))
} }
func (a *PlaylistsPage) CreateRenderer() fyne.WidgetRenderer { func (a *PlaylistsPage) CreateRenderer() fyne.WidgetRenderer {