Merge pull request #355 from adamantike/misc/migrate-artists-page-to-gridview
misc: Migrate Artists page to GridView
This commit is contained in:
@@ -64,6 +64,10 @@ type ArtistPageConfig struct {
|
||||
TracklistColumns []string
|
||||
}
|
||||
|
||||
type ArtistsPageConfig struct {
|
||||
SortOrder string
|
||||
}
|
||||
|
||||
type FavoritesPageConfig struct {
|
||||
InitialView string
|
||||
TracklistColumns []string
|
||||
@@ -122,6 +126,7 @@ type Config struct {
|
||||
AlbumPage AlbumPageConfig
|
||||
AlbumsPage AlbumsPageConfig
|
||||
ArtistPage ArtistPageConfig
|
||||
ArtistsPage ArtistsPageConfig
|
||||
FavoritesPage FavoritesPageConfig
|
||||
PlaylistPage PlaylistPageConfig
|
||||
PlaylistsPage PlaylistsPageConfig
|
||||
@@ -163,6 +168,9 @@ func DefaultConfig(appVersionTag string) *Config {
|
||||
InitialView: "Discography",
|
||||
TracklistColumns: []string{"Album", "Time", "Plays", "Favorite", "Rating"},
|
||||
},
|
||||
ArtistsPage: ArtistsPageConfig{
|
||||
SortOrder: string("Name (A-Z)"),
|
||||
},
|
||||
FavoritesPage: FavoritesPageConfig{
|
||||
TracklistColumns: []string{"Artist", "Album", "Time", "Plays"},
|
||||
InitialView: "Albums",
|
||||
|
||||
@@ -29,10 +29,11 @@ func NewAlbumIterator(fetchFn AlbumFetchFn, filter mediaprovider.AlbumFilter, cb
|
||||
|
||||
type ArtistFetchFn func(offset, limit int) ([]*mediaprovider.Artist, error)
|
||||
|
||||
func NewArtistIterator(fetchFn ArtistFetchFn) mediaprovider.ArtistIterator {
|
||||
return &baseIter[mediaprovider.Artist, nilFilterOptions]{
|
||||
fetcher: fetchFn,
|
||||
filter: nilFilter[mediaprovider.Artist]{},
|
||||
func NewArtistIterator(fetchFn ArtistFetchFn, filter mediaprovider.ArtistFilter, cb func(string)) mediaprovider.ArtistIterator {
|
||||
return &baseIter[mediaprovider.Artist, mediaprovider.ArtistFilterOptions]{
|
||||
prefetchCB: func(a *mediaprovider.Artist) { cb(a.CoverArtID) },
|
||||
fetcher: fetchFn,
|
||||
filter: filter,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ func (j *jellyfinMediaProvider) IterateTracks(searchQuery string) mediaprovider.
|
||||
return helpers.NewTrackIterator(fetcher, j.prefetchCoverCB)
|
||||
}
|
||||
|
||||
func (j *jellyfinMediaProvider) IterateArtists(sortOrder string) mediaprovider.ArtistIterator {
|
||||
func (j *jellyfinMediaProvider) IterateArtists(sortOrder string, filter mediaprovider.ArtistFilter) mediaprovider.ArtistIterator {
|
||||
var jfSort jellyfin.Sort
|
||||
|
||||
if sortOrder == "" {
|
||||
@@ -147,7 +147,18 @@ func (j *jellyfinMediaProvider) IterateArtists(sortOrder string) mediaprovider.A
|
||||
return sharedutil.MapSlice(ar, toArtist), nil
|
||||
}
|
||||
|
||||
return helpers.NewArtistIterator(fetcher)
|
||||
return helpers.NewArtistIterator(fetcher, filter, j.prefetchCoverCB)
|
||||
}
|
||||
|
||||
func (j *jellyfinMediaProvider) SearchArtists(searchQuery string, filter mediaprovider.ArtistFilter) mediaprovider.ArtistIterator {
|
||||
fetcher := func(offs, limit int) ([]*mediaprovider.Artist, error) {
|
||||
sr, err := j.client.Search(searchQuery, jellyfin.TypeArtist, jellyfin.Paging{StartIndex: offs, Limit: limit})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sharedutil.MapSlice(sr.Artists, toArtist), nil
|
||||
}
|
||||
return helpers.NewArtistIterator(fetcher, filter, j.prefetchCoverCB)
|
||||
}
|
||||
|
||||
// Creates the Jellyfin filter to implement the given mediaprovider filter,
|
||||
|
||||
@@ -94,6 +94,48 @@ func (f albumFilter) Matches(album *Album) bool {
|
||||
return genresMatch(f.options.Genres, album.Genres)
|
||||
}
|
||||
|
||||
type ArtistFilter = MediaFilter[Artist, ArtistFilterOptions]
|
||||
|
||||
type ArtistFilterOptions struct{}
|
||||
|
||||
// Clone returns a deep copy of the filter options
|
||||
func (o ArtistFilterOptions) Clone() ArtistFilterOptions {
|
||||
return ArtistFilterOptions{}
|
||||
}
|
||||
|
||||
type artistFilter struct {
|
||||
options ArtistFilterOptions
|
||||
}
|
||||
|
||||
func NewArtistFilter(options ArtistFilterOptions) *artistFilter {
|
||||
return &artistFilter{options}
|
||||
}
|
||||
|
||||
func (a artistFilter) Options() ArtistFilterOptions {
|
||||
return a.options
|
||||
}
|
||||
|
||||
func (a *artistFilter) SetOptions(o ArtistFilterOptions) {
|
||||
a.options = o
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of the filter
|
||||
func (a artistFilter) Clone() ArtistFilter {
|
||||
return NewArtistFilter(a.options.Clone())
|
||||
}
|
||||
|
||||
// Returns true if the filter is the nil filter - i.e. matches everything
|
||||
func (a artistFilter) IsNil() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (f artistFilter) Matches(artist *Artist) bool {
|
||||
if artist == nil {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type RatingFavoriteParameters struct {
|
||||
AlbumIDs []string
|
||||
ArtistIDs []string
|
||||
@@ -149,7 +191,9 @@ type MediaProvider interface {
|
||||
|
||||
ArtistSortOrders() []string
|
||||
|
||||
IterateArtists(sortOrder string) ArtistIterator
|
||||
IterateArtists(sortOrder string, filter ArtistFilter) ArtistIterator
|
||||
|
||||
SearchArtists(searchQuery string, filter ArtistFilter) ArtistIterator
|
||||
|
||||
GetGenres() ([]*Genre, error)
|
||||
|
||||
|
||||
@@ -23,7 +23,14 @@ func (s *subsonicMediaProvider) ArtistSortOrders() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *subsonicMediaProvider) IterateArtists(sortOrder string) mediaprovider.ArtistIterator {
|
||||
func filterArtistMatches(f mediaprovider.ArtistFilter, artist *subsonic.ArtistID3) bool {
|
||||
if artist == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *subsonicMediaProvider) IterateArtists(sortOrder string, filter mediaprovider.ArtistFilter) mediaprovider.ArtistIterator {
|
||||
if sortOrder == "" {
|
||||
sortOrder = ArtistSortNameAZ // default
|
||||
}
|
||||
@@ -37,6 +44,7 @@ func (s *subsonicMediaProvider) IterateArtists(sortOrder string) mediaprovider.A
|
||||
})
|
||||
return artists
|
||||
},
|
||||
filter,
|
||||
)
|
||||
default:
|
||||
log.Printf("Undefined artist sort order: %s", sortOrder)
|
||||
@@ -44,8 +52,85 @@ func (s *subsonicMediaProvider) IterateArtists(sortOrder string) mediaprovider.A
|
||||
}
|
||||
}
|
||||
|
||||
func (s *subsonicMediaProvider) baseArtistIterFromSimpleSortOrder(sortFn func([]*subsonic.ArtistID3) []*subsonic.ArtistID3) mediaprovider.ArtistIterator {
|
||||
return helpers.NewArtistIterator(s.artistFetchFnFromStandardSort(sortFn))
|
||||
func (s *subsonicMediaProvider) SearchArtists(searchQuery string, filter mediaprovider.ArtistFilter) mediaprovider.ArtistIterator {
|
||||
return s.newSearchArtistIter(searchQuery, filter, s.prefetchCoverCB)
|
||||
}
|
||||
|
||||
type searchArtistIter struct {
|
||||
searchIterBase
|
||||
|
||||
prefetchCB func(string)
|
||||
filter mediaprovider.ArtistFilter
|
||||
prefetched []*subsonic.ArtistID3
|
||||
prefetchedPos int
|
||||
artistIDset map[string]bool
|
||||
done bool
|
||||
}
|
||||
|
||||
func (s *subsonicMediaProvider) newSearchArtistIter(query string, filter mediaprovider.ArtistFilter, cb func(string)) *searchArtistIter {
|
||||
return &searchArtistIter{
|
||||
searchIterBase: searchIterBase{
|
||||
query: query,
|
||||
s: s.client,
|
||||
},
|
||||
prefetchCB: cb,
|
||||
filter: filter,
|
||||
artistIDset: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *searchArtistIter) Next() *mediaprovider.Artist {
|
||||
if s.done {
|
||||
return nil
|
||||
}
|
||||
|
||||
// prefetch more search results from server
|
||||
if s.prefetched == nil {
|
||||
results := s.searchIterBase.fetchResults()
|
||||
if results == nil {
|
||||
s.done = true
|
||||
s.artistIDset = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// add results from artists search
|
||||
s.addNewArtists(results.Artist)
|
||||
s.artistOffset += len(results.Artist)
|
||||
}
|
||||
|
||||
// return from prefetched results
|
||||
if len(s.prefetched) > 0 {
|
||||
a := s.prefetched[s.prefetchedPos]
|
||||
s.prefetchedPos++
|
||||
if s.prefetchedPos == len(s.prefetched) {
|
||||
s.prefetched = nil
|
||||
s.prefetchedPos = 0
|
||||
}
|
||||
|
||||
return toArtistFromID3(a)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *searchArtistIter) addNewArtists(artists []*subsonic.ArtistID3) {
|
||||
for _, artist := range artists {
|
||||
if _, have := s.artistIDset[artist.ID]; have {
|
||||
continue
|
||||
}
|
||||
if !filterArtistMatches(s.filter, artist) {
|
||||
continue
|
||||
}
|
||||
s.prefetched = append(s.prefetched, artist)
|
||||
if s.prefetchCB != nil {
|
||||
go s.prefetchCB(artist.CoverArt)
|
||||
}
|
||||
s.artistIDset[artist.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *subsonicMediaProvider) baseArtistIterFromSimpleSortOrder(sortFn func([]*subsonic.ArtistID3) []*subsonic.ArtistID3, filter mediaprovider.ArtistFilter) mediaprovider.ArtistIterator {
|
||||
return helpers.NewArtistIterator(s.artistFetchFnFromStandardSort(sortFn), filter, s.prefetchCoverCB)
|
||||
}
|
||||
|
||||
func (s *subsonicMediaProvider) artistFetchFnFromStandardSort(sortFn func([]*subsonic.ArtistID3) []*subsonic.ArtistID3) helpers.ArtistFetchFn {
|
||||
|
||||
@@ -164,20 +164,22 @@ func (s *ServerManager) connect(connection ServerConnection, password string) (m
|
||||
if connection.ServerType == ServerTypeJellyfin {
|
||||
client, err := jellyfin.NewClient(connection.Hostname, res.AppName, res.AppVersion, jellyfin.WithTimeout(10*time.Second))
|
||||
if err != nil {
|
||||
log.Print("Error creating Jellyfin client")
|
||||
log.Printf("error creating Jellyfin client: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
cli = &jellyfinMP.JellyfinServer{
|
||||
Client: *client,
|
||||
}
|
||||
|
||||
altClient, err := jellyfin.NewClient(connection.AltHostname, res.AppName, res.AppVersion, jellyfin.WithTimeout(10*time.Second))
|
||||
if err != nil {
|
||||
log.Print("Error creating Jellyfin alternative client")
|
||||
return nil, err
|
||||
}
|
||||
altCli = &jellyfinMP.JellyfinServer{
|
||||
Client: *altClient,
|
||||
if connection.AltHostname != "" {
|
||||
altClient, err := jellyfin.NewClient(connection.AltHostname, res.AppName, res.AppVersion, jellyfin.WithTimeout(10*time.Second))
|
||||
if err != nil {
|
||||
log.Printf("error creating Jellyfin alternative client: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
altCli = &jellyfinMP.JellyfinServer{
|
||||
Client: *altClient,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cli = &subsonicMP.SubsonicServer{
|
||||
|
||||
@@ -6,7 +6,7 @@ require (
|
||||
fyne.io/fyne/v2 v2.4.4
|
||||
github.com/20after4/configdir v0.1.1
|
||||
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1
|
||||
github.com/dweymouth/go-jellyfin v0.0.0-20240324234906-fd6aa188b773
|
||||
github.com/dweymouth/go-jellyfin v0.0.0-20240330010648-fb02c0b3878e
|
||||
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee
|
||||
github.com/dweymouth/go-subsonic v0.0.0-20240305034202-6193dca1c9de
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
|
||||
@@ -71,8 +71,8 @@ github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 h1:mGvOb3zxl4vCLv+
|
||||
github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1/go.mod h1:ZNCLJfehvEf34B7BbLKjgpsL9lyW7q938w/GY1XgV4E=
|
||||
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20240313160419-e8b6f75cfa12 h1:fCY8VgSZMau2383XeRkVhQHIm+mgWSpuldDKHYD+HG4=
|
||||
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20240313160419-e8b6f75cfa12/go.mod h1:VyrxAOZ3NRZRWBvNIJbfqoKOG4DdbewoPk7ozqJKNPY=
|
||||
github.com/dweymouth/go-jellyfin v0.0.0-20240324234906-fd6aa188b773 h1:nM5gPfIvwIIhz28TzYQZE09N0lmQp9E8pJVm2sI1Wnc=
|
||||
github.com/dweymouth/go-jellyfin v0.0.0-20240324234906-fd6aa188b773/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI=
|
||||
github.com/dweymouth/go-jellyfin v0.0.0-20240330010648-fb02c0b3878e h1:89N7tfmGPA3kB3JPhm0UYbc2fjIMUgHPryj9jeuaeTg=
|
||||
github.com/dweymouth/go-jellyfin v0.0.0-20240330010648-fb02c0b3878e/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI=
|
||||
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31BugypcF/TPKEy2RrGR9JFq1JOjOpY=
|
||||
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee/go.mod h1:Ov0ieN90M7i+0k3OxhA/g1dozGs+UcPHDsMKqPgRDk0=
|
||||
github.com/dweymouth/go-subsonic v0.0.0-20240305034202-6193dca1c9de h1:RVYHvjT01YSLphWlTA/uaGtfVp2x6/4UfOIYYp9vMis=
|
||||
|
||||
+53
-198
@@ -1,223 +1,78 @@
|
||||
package browsing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"slices"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"github.com/dweymouth/supersonic/backend"
|
||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||
"github.com/dweymouth/supersonic/sharedutil"
|
||||
"github.com/dweymouth/supersonic/ui/controller"
|
||||
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
||||
"github.com/dweymouth/supersonic/ui/util"
|
||||
"github.com/dweymouth/supersonic/ui/widgets"
|
||||
)
|
||||
|
||||
type ArtistsPage struct {
|
||||
widget.BaseWidget
|
||||
|
||||
contr *controller.Controller
|
||||
pool *util.WidgetPool
|
||||
im *backend.ImageManager
|
||||
pm *backend.PlaybackManager
|
||||
mp mediaprovider.MediaProvider
|
||||
artists []*mediaprovider.Artist
|
||||
searchedArtists []*mediaprovider.Artist
|
||||
grid *widgets.GridView
|
||||
fullGridScrollPos float32
|
||||
searchGridScrollPos float32
|
||||
searcher *widgets.SearchEntry
|
||||
searchText string
|
||||
titleDisp *widget.RichText
|
||||
|
||||
container *fyne.Container
|
||||
type artistsPageAdapter struct {
|
||||
cfg *backend.ArtistsPageConfig
|
||||
contr *controller.Controller
|
||||
mp mediaprovider.MediaProvider
|
||||
pm *backend.PlaybackManager
|
||||
filter mediaprovider.ArtistFilter
|
||||
}
|
||||
|
||||
func NewArtistsPage(
|
||||
contr *controller.Controller,
|
||||
pool *util.WidgetPool,
|
||||
pm *backend.PlaybackManager,
|
||||
mp mediaprovider.MediaProvider,
|
||||
im *backend.ImageManager,
|
||||
) *ArtistsPage {
|
||||
return newArtistsPage(contr, pool, pm, mp, im, "", 0, 0)
|
||||
func NewArtistsPage(cfg *backend.ArtistsPageConfig, pool *util.WidgetPool, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) Page {
|
||||
adapter := &artistsPageAdapter{cfg: cfg, contr: contr, mp: mp, pm: pm}
|
||||
return NewGridViewPage(adapter, pool, mp, im)
|
||||
}
|
||||
|
||||
func newArtistsPage(
|
||||
contr *controller.Controller,
|
||||
pool *util.WidgetPool,
|
||||
pm *backend.PlaybackManager,
|
||||
mp mediaprovider.MediaProvider,
|
||||
im *backend.ImageManager,
|
||||
searchText string,
|
||||
fullGridScrollPos float32,
|
||||
searchGridScrollPos float32,
|
||||
) *ArtistsPage {
|
||||
a := &ArtistsPage{
|
||||
contr: contr,
|
||||
pool: pool,
|
||||
pm: pm,
|
||||
mp: mp,
|
||||
im: im,
|
||||
searchText: searchText,
|
||||
fullGridScrollPos: fullGridScrollPos,
|
||||
searchGridScrollPos: searchGridScrollPos,
|
||||
}
|
||||
a.ExtendBaseWidget(a)
|
||||
func (a *artistsPageAdapter) Title() string { return "Artists" }
|
||||
|
||||
a.titleDisp = widget.NewRichTextWithText("Artists")
|
||||
a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{
|
||||
SizeName: theme.SizeNameHeadingText,
|
||||
func (a *artistsPageAdapter) Filter() mediaprovider.ArtistFilter {
|
||||
if a.filter == nil {
|
||||
a.filter = mediaprovider.NewArtistFilter(
|
||||
mediaprovider.ArtistFilterOptions{},
|
||||
)
|
||||
}
|
||||
a.searcher = widgets.NewSearchEntry()
|
||||
a.searcher.PlaceHolder = "Search page"
|
||||
a.searcher.OnSearched = func(query string) { a.onSearched(query, false /*firstLoad*/) }
|
||||
a.searcher.Entry.Text = searchText
|
||||
if g := pool.Obtain(util.WidgetTypeGridView); g != nil {
|
||||
a.grid = g.(*widgets.GridView)
|
||||
a.grid.Placeholder = myTheme.ArtistIcon
|
||||
a.grid.Clear()
|
||||
} else {
|
||||
a.grid = widgets.NewFixedGridView(nil, a.im, myTheme.ArtistIcon)
|
||||
return a.filter
|
||||
}
|
||||
|
||||
func (a *artistsPageAdapter) FilterButton() widgets.FilterButton[mediaprovider.Artist, mediaprovider.ArtistFilterOptions] {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *artistsPageAdapter) PlaceholderResource() fyne.Resource { return myTheme.ArtistIcon }
|
||||
|
||||
func (a *artistsPageAdapter) Route() controller.Route { return controller.ArtistsRoute() }
|
||||
|
||||
func (a *artistsPageAdapter) SortOrders() ([]string, string) {
|
||||
orders := a.mp.ArtistSortOrders()
|
||||
sortOrder := a.cfg.SortOrder
|
||||
if !slices.Contains(orders, sortOrder) {
|
||||
sortOrder = string(orders[0])
|
||||
}
|
||||
return orders, sortOrder
|
||||
}
|
||||
|
||||
func (a *artistsPageAdapter) SaveSortOrder(order string) {
|
||||
a.cfg.SortOrder = order
|
||||
}
|
||||
|
||||
func (a *artistsPageAdapter) ActionButton() *widget.Button { return nil }
|
||||
|
||||
func (a *artistsPageAdapter) Iter(sortOrder string, filter mediaprovider.ArtistFilter) widgets.GridViewIterator {
|
||||
return widgets.NewGridViewArtistIterator(a.mp.IterateArtists(sortOrder, filter))
|
||||
}
|
||||
|
||||
func (a *artistsPageAdapter) SearchIter(query string, filter mediaprovider.ArtistFilter) widgets.GridViewIterator {
|
||||
return widgets.NewGridViewArtistIterator(a.mp.SearchArtists(query, filter))
|
||||
}
|
||||
|
||||
func (a *artistsPageAdapter) ConnectGridActions(gv *widgets.GridView) {
|
||||
canShareArtists := false
|
||||
if r, canShare := mp.(mediaprovider.SupportsSharing); canShare {
|
||||
if r, canShare := a.mp.(mediaprovider.SupportsSharing); canShare {
|
||||
canShareArtists = r.CanShareArtists()
|
||||
}
|
||||
a.grid.DisableSharing = !canShareArtists
|
||||
a.contr.ConnectArtistGridActions(a.grid)
|
||||
|
||||
searchVbox := container.NewVBox(layout.NewSpacer(), a.searcher, layout.NewSpacer())
|
||||
a.container = container.NewBorder(
|
||||
container.NewHBox(util.NewHSpace(6),
|
||||
a.titleDisp, layout.NewSpacer(), searchVbox, util.NewHSpace(15)),
|
||||
nil, nil, nil, a.grid,
|
||||
)
|
||||
|
||||
go a.load()
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *ArtistsPage) Reload() {
|
||||
a.searchGridScrollPos = 0
|
||||
a.fullGridScrollPos = 0
|
||||
go a.load()
|
||||
}
|
||||
|
||||
func (a *ArtistsPage) load() {
|
||||
iter := a.mp.IterateArtists("")
|
||||
var artists []*mediaprovider.Artist
|
||||
for {
|
||||
artist := iter.Next()
|
||||
if artist == nil {
|
||||
break
|
||||
}
|
||||
artists = append(artists, artist)
|
||||
}
|
||||
a.artists = artists
|
||||
a.onSearched(a.searcher.Entry.Text, true)
|
||||
}
|
||||
|
||||
var _ Searchable = (*ArtistsPage)(nil)
|
||||
|
||||
func (a *ArtistsPage) SearchWidget() fyne.Focusable {
|
||||
return a.searcher
|
||||
}
|
||||
|
||||
func (a *ArtistsPage) onSearched(query string, firstLoad bool) {
|
||||
// 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
|
||||
var artists []*mediaprovider.Artist
|
||||
scrollPos := float32(0)
|
||||
if query == "" {
|
||||
a.searchedArtists = nil
|
||||
artists = a.artists
|
||||
scrollPos = a.fullGridScrollPos
|
||||
} else {
|
||||
if firstLoad { // if reloading with a saved search state, set scroll position
|
||||
scrollPos = a.searchGridScrollPos
|
||||
}
|
||||
if a.searchText == "" {
|
||||
// if first search, capture scroll position of full, unsearched grid
|
||||
a.fullGridScrollPos = a.grid.GetScrollOffset()
|
||||
}
|
||||
qLower := strings.ToLower(query)
|
||||
a.searchedArtists = sharedutil.FilterSlice(a.artists, func(p *mediaprovider.Artist) bool {
|
||||
return strings.Contains(strings.ToLower(p.Name), qLower)
|
||||
})
|
||||
artists = a.searchedArtists
|
||||
}
|
||||
a.searchText = query
|
||||
a.grid.ResetFixed(createArtistsGridViewModel(artists))
|
||||
a.grid.Refresh()
|
||||
a.grid.ScrollToOffset(scrollPos)
|
||||
}
|
||||
|
||||
func createArtistsGridViewModel(artists []*mediaprovider.Artist) []widgets.GridViewItemModel {
|
||||
return sharedutil.MapSlice(artists, func(ar *mediaprovider.Artist) widgets.GridViewItemModel {
|
||||
albums := "albums"
|
||||
if ar.AlbumCount == 1 {
|
||||
albums = "album"
|
||||
}
|
||||
return widgets.GridViewItemModel{
|
||||
Name: ar.Name,
|
||||
ID: ar.ID,
|
||||
CoverArtID: ar.CoverArtID,
|
||||
Secondary: []string{fmt.Sprintf("%d %s", ar.AlbumCount, albums)},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var _ Scrollable = (*ArtistsPage)(nil)
|
||||
|
||||
func (a *ArtistsPage) Scroll(scrollAmt float32) {
|
||||
a.grid.ScrollToOffset(a.grid.GetScrollOffset() + scrollAmt)
|
||||
}
|
||||
|
||||
func (a *ArtistsPage) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(a.container)
|
||||
}
|
||||
|
||||
func (a *ArtistsPage) Route() controller.Route {
|
||||
return controller.ArtistsRoute()
|
||||
}
|
||||
|
||||
func (a *ArtistsPage) Save() SavedPage {
|
||||
s := &savedArtistsPage{
|
||||
contr: a.contr,
|
||||
pool: a.pool,
|
||||
im: a.im,
|
||||
pm: a.pm,
|
||||
mp: a.mp,
|
||||
searchText: a.searchText,
|
||||
fullGridScrollPos: a.fullGridScrollPos,
|
||||
}
|
||||
if a.searchText == "" {
|
||||
s.fullGridScrollPos = a.grid.GetScrollOffset()
|
||||
} else {
|
||||
s.searchGridScrollPos = a.grid.GetScrollOffset()
|
||||
}
|
||||
a.grid.Clear()
|
||||
a.pool.Release(util.WidgetTypeGridView, a.grid)
|
||||
return s
|
||||
}
|
||||
|
||||
type savedArtistsPage struct {
|
||||
contr *controller.Controller
|
||||
pool *util.WidgetPool
|
||||
im *backend.ImageManager
|
||||
pm *backend.PlaybackManager
|
||||
mp mediaprovider.MediaProvider
|
||||
searchText string
|
||||
fullGridScrollPos float32
|
||||
searchGridScrollPos float32
|
||||
}
|
||||
|
||||
func (s *savedArtistsPage) Restore() Page {
|
||||
return newArtistsPage(s.contr, s.pool, s.pm, s.mp, s.im, s.searchText, s.fullGridScrollPos, s.searchGridScrollPos)
|
||||
gv.DisableSharing = !canShareArtists
|
||||
a.contr.ConnectArtistGridActions(gv)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func (r Router) CreatePage(rte controller.Route) Page {
|
||||
case controller.Artist:
|
||||
return NewArtistPage(rte.Arg, &r.App.Config.ArtistPage, r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller)
|
||||
case controller.Artists:
|
||||
return NewArtistsPage(r.Controller, r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager)
|
||||
return NewArtistsPage(&r.App.Config.ArtistsPage, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager)
|
||||
case controller.Favorites:
|
||||
return NewFavoritesPage(&r.App.Config.FavoritesPage, r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager)
|
||||
case controller.Genre:
|
||||
|
||||
@@ -220,6 +220,9 @@ func (m *Controller) ConnectArtistGridActions(grid *widgets.GridView) {
|
||||
m.ShowDownloadDialog(tracks, artist.Name)
|
||||
}()
|
||||
}
|
||||
grid.OnShare = func(artistID string) {
|
||||
go m.ShowShareDialog(artistID)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Controller) GetArtistTracks(artistID string) []*mediaprovider.Track {
|
||||
|
||||
@@ -2,6 +2,7 @@ package widgets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
@@ -65,6 +66,30 @@ func NewGridViewAlbumIterator(iter mediaprovider.AlbumIterator) GridViewIterator
|
||||
return gridViewAlbumIterator{iter: NewBatchingIterator(iter)}
|
||||
}
|
||||
|
||||
type gridViewArtistIterator struct {
|
||||
iter BatchingIterator[mediaprovider.Artist]
|
||||
}
|
||||
|
||||
func (g gridViewArtistIterator) NextN(n int) []GridViewItemModel {
|
||||
artists := g.iter.NextN(n)
|
||||
return sharedutil.MapSlice(artists, func(ar *mediaprovider.Artist) GridViewItemModel {
|
||||
albumsLabel := "albums"
|
||||
if ar.AlbumCount == 1 {
|
||||
albumsLabel = "album"
|
||||
}
|
||||
return GridViewItemModel{
|
||||
Name: ar.Name,
|
||||
ID: ar.ID,
|
||||
CoverArtID: ar.CoverArtID,
|
||||
Secondary: []string{fmt.Sprintf("%d %s", ar.AlbumCount, albumsLabel)},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func NewGridViewArtistIterator(iter mediaprovider.ArtistIterator) GridViewIterator {
|
||||
return gridViewArtistIterator{iter: NewBatchingIterator(iter)}
|
||||
}
|
||||
|
||||
type GridView struct {
|
||||
widget.BaseWidget
|
||||
|
||||
|
||||
Reference in New Issue
Block a user