diff --git a/backend/mediaprovider/model.go b/backend/mediaprovider/model.go index d4e2d36..d6ffa63 100644 --- a/backend/mediaprovider/model.go +++ b/backend/mediaprovider/model.go @@ -20,6 +20,7 @@ type AlbumWithTracks struct { type Artist struct { ID string + CoverArtID string Name string Favorite bool AlbumCount int diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index 2c071bc..b927040 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -311,6 +311,7 @@ func toArtistFromID3(ar *subsonic.ArtistID3) *mediaprovider.Artist { } return &mediaprovider.Artist{ ID: ar.ID, + CoverArtID: ar.CoverArt, Name: ar.Name, Favorite: !ar.Starred.IsZero(), AlbumCount: ar.AlbumCount, diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index e1a4c95..7e863a9 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -126,15 +126,6 @@ func (a *ArtistPage) OnSongChange(track, lastScrobbledIfAny *mediaprovider.Track } } -func (a *ArtistPage) playAllTracks() { - if a.artistInfo != nil { // page loaded - for i, album := range a.artistInfo.Albums { - a.pm.LoadAlbum(album.ID, i > 0 /*append*/, false /*shuffle*/) - } - a.pm.PlayFromBeginning() - } -} - func (a *ArtistPage) playArtistRadio() { go a.pm.PlaySimilarSongs(a.artistID) } @@ -267,7 +258,9 @@ func NewArtistPageHeader(page *ArtistPage) *ArtistPageHeader { } } a.favoriteBtn = widgets.NewFavoriteButton(func() { go a.toggleFavorited() }) - a.playBtn = widget.NewButtonWithIcon("Play Discography", theme.MediaPlayIcon(), page.playAllTracks) + a.playBtn = widget.NewButtonWithIcon("Play Discography", theme.MediaPlayIcon(), func() { + go a.artistPage.contr.PlayArtistDiscography(a.artistID, false /*shuffle*/) + }) a.playRadioBtn = widget.NewButtonWithIcon(" Play Artist Radio", myTheme.ShuffleIcon, page.playArtistRadio) a.biographyDisp.Wrapping = fyne.TextWrapWord a.ExtendBaseWidget(a) diff --git a/ui/browsing/artistspage.go b/ui/browsing/artistspage.go new file mode 100644 index 0000000..bfeed05 --- /dev/null +++ b/ui/browsing/artistspage.go @@ -0,0 +1,199 @@ +package browsing + +import ( + "fmt" + "log" + "strings" + + "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" + "github.com/dweymouth/supersonic/ui/util" + "github.com/dweymouth/supersonic/ui/widgets" +) + +type ArtistsPage struct { + widget.BaseWidget + + contr *controller.Controller + 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 +} + +func NewArtistsPage( + contr *controller.Controller, + pm *backend.PlaybackManager, + mp mediaprovider.MediaProvider, + im *backend.ImageManager, +) *ArtistsPage { + return newArtistsPage(contr, pm, mp, im, "", 0, 0) +} + +func newArtistsPage( + contr *controller.Controller, + pm *backend.PlaybackManager, + mp mediaprovider.MediaProvider, + im *backend.ImageManager, + searchText string, + fullGridScrollPos float32, + searchGridScrollPos float32, +) *ArtistsPage { + a := &ArtistsPage{ + contr: contr, + pm: pm, + mp: mp, + im: im, + searchText: searchText, + fullGridScrollPos: fullGridScrollPos, + searchGridScrollPos: searchGridScrollPos, + } + a.ExtendBaseWidget(a) + + log.Printf("Scroll pos: full %0.2f search %0.2f", fullGridScrollPos, searchGridScrollPos) + a.titleDisp = widget.NewRichTextWithText("Artists") + a.titleDisp.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{ + SizeName: theme.SizeNameHeadingText, + } + a.searcher = widgets.NewSearchEntry() + a.searcher.OnSearched = func(query string) { a.onSearched(query, false /*firstLoad*/) } + a.searcher.Entry.Text = searchText + a.grid = widgets.NewFixedGridView(nil, a.im) + a.grid.OnShowItemPage = a.showArtistPage + a.grid.OnPlay = func(artistID string, shuffle bool) { go a.contr.PlayArtistDiscography(artistID, shuffle) } + a.grid.OnAddToQueue = func(artistID string) { + go a.pm.LoadTracks(a.contr.GetArtistTracks(artistID), true /*append*/, false /*shuffle*/) + } + a.grid.OnAddToPlaylist = func(artistID string) { + go a.contr.DoAddTracksToPlaylistWorkflow( + sharedutil.TracksToIDs(a.contr.GetArtistTracks(artistID))) + } + + 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() { + artists, err := a.mp.GetArtists() + if err != nil { + log.Printf("error loading artists: %v", err.Error()) + } + a.artists = artists + a.onSearched(a.searcher.Entry.Text, true) +} + +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: fmt.Sprintf("%d %s", ar.AlbumCount, albums), + } + }) +} + +func (a *ArtistsPage) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(a.container) +} + +func (a *ArtistsPage) Route() controller.Route { + return controller.ArtistsRoute() +} + +func (a *ArtistsPage) showArtistPage(id string) { + a.contr.NavigateTo(controller.ArtistRoute(id)) +} + +func (a *ArtistsPage) Save() SavedPage { + s := &savedArtistsPage{ + contr: a.contr, + 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() + } + return s +} + +type savedArtistsPage struct { + contr *controller.Controller + 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.pm, s.mp, s.im, s.searchText, s.fullGridScrollPos, s.searchGridScrollPos) +} diff --git a/ui/browsing/genrepage.go b/ui/browsing/genrepage.go index 6686373..cab09e8 100644 --- a/ui/browsing/genrepage.go +++ b/ui/browsing/genrepage.go @@ -33,8 +33,6 @@ type GenrePage struct { titleDisp *widget.RichText playRandom *widget.Button - OnPlayAlbum func(string, int) - container *fyne.Container } diff --git a/ui/browsing/router.go b/ui/browsing/router.go index c337347..2ab39e7 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -34,7 +34,7 @@ func (r Router) CreatePage(rte controller.Route) Page { case controller.Artist: return NewArtistPage(rte.Arg, &r.App.Config.ArtistPage, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller) case controller.Artists: - return NewArtistsGenresPage(false, r.Controller, r.App.ServerManager.Server) + return NewArtistsPage(r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Favorites: return NewFavoritesPage(&r.App.Config.FavoritesPage, 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 e340cfc..9b098f1 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -140,6 +140,35 @@ func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) { } } +func (m *Controller) GetArtistTracks(artistID string) []*mediaprovider.Track { + server := m.App.ServerManager.Server + if server == nil { + log.Println("error playing artist discography: logged out") + return nil + } + + artist, err := server.GetArtist(artistID) + if err != nil { + log.Printf("error getting artist discography: %v", err.Error()) + return nil + } + var allTracks []*mediaprovider.Track + for _, album := range artist.Albums { + album, err := server.GetAlbum(album.ID) + if err != nil { + log.Printf("error loading album tracks: %v", err.Error()) + return nil + } + allTracks = append(allTracks, album.Tracks...) + } + return allTracks +} + +func (m *Controller) PlayArtistDiscography(artistID string, shuffle bool) { + m.App.PlaybackManager.LoadTracks(m.GetArtistTracks(artistID), false, shuffle) + m.App.PlaybackManager.PlayFromBeginning() +} + func (m *Controller) PromptForFirstServer() { d := dialogs.NewAddEditServerDialog("Connect to Server", false, nil, m.MainWindow.Canvas().Focus) pop := widget.NewModalPopUp(d, m.MainWindow.Canvas())