diff --git a/backend/config.go b/backend/config.go index 7af2c7b..dcb8402 100644 --- a/backend/config.go +++ b/backend/config.go @@ -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", diff --git a/backend/mediaprovider/helpers/iterators.go b/backend/mediaprovider/helpers/iterators.go index a7e9778..e65379d 100644 --- a/backend/mediaprovider/helpers/iterators.go +++ b/backend/mediaprovider/helpers/iterators.go @@ -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, } } diff --git a/backend/mediaprovider/jellyfin/iterators.go b/backend/mediaprovider/jellyfin/iterators.go index c6c58a9..33d38f1 100644 --- a/backend/mediaprovider/jellyfin/iterators.go +++ b/backend/mediaprovider/jellyfin/iterators.go @@ -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, diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index 8bfec98..b476e0c 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -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) diff --git a/backend/mediaprovider/subsonic/artistiterator.go b/backend/mediaprovider/subsonic/artistiterator.go index b0688fa..ebe606d 100644 --- a/backend/mediaprovider/subsonic/artistiterator.go +++ b/backend/mediaprovider/subsonic/artistiterator.go @@ -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 { diff --git a/backend/servermanager.go b/backend/servermanager.go index 0e8483d..82ca80e 100644 --- a/backend/servermanager.go +++ b/backend/servermanager.go @@ -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{ diff --git a/go.mod b/go.mod index a386531..6c6395a 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index a598e6e..b2bec26 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/ui/browsing/artistspage.go b/ui/browsing/artistspage.go index b34de3b..0e29818 100644 --- a/ui/browsing/artistspage.go +++ b/ui/browsing/artistspage.go @@ -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) } diff --git a/ui/browsing/router.go b/ui/browsing/router.go index 8896c9e..26c2af4 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -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: diff --git a/ui/controller/controller.go b/ui/controller/controller.go index d64bc61..d2debc8 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -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 { diff --git a/ui/widgets/gridview.go b/ui/widgets/gridview.go index fbe4b25..59015eb 100644 --- a/ui/widgets/gridview.go +++ b/ui/widgets/gridview.go @@ -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