From c0fcb633cf0b3500a33590c710dac44703122459 Mon Sep 17 00:00:00 2001 From: zacaj Date: Sat, 17 Jan 2026 21:53:00 -0500 Subject: [PATCH 1/3] All Tracks tab for Artist page --- ui/browsing/artistpage.go | 193 ++++++++++++++++++++++++++++---------- 1 file changed, 146 insertions(+), 47 deletions(-) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 9989db0..e5fa289 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -31,9 +31,10 @@ var ( var _ fyne.Widget = (*ArtistPage)(nil) type artistPageState struct { - artistID string - activeView int - trackSort widgets.TracklistSort + artistID string + activeView int + topTrackSort widgets.TracklistSort + allTrackSort widgets.TracklistSort pool *util.WidgetPool cfg *backend.ArtistPageConfig @@ -42,8 +43,9 @@ type artistPageState struct { im *backend.ImageManager contr *controller.Controller - gridScrollPos float32 // for album grid (or grouped releases) - listScrollPos float32 // for Top Tracks list + gridScrollPos float32 // for album grid (or grouped releases) + topListScrollPos float32 // for Top Tracks list + allListScrollPos float32 // for All Tracks list sectionVis widgets.GroupedReleasesSectionVisibility sectionVisNeedApply bool @@ -69,13 +71,21 @@ type ArtistPage struct { const ( viewTopTracks = "Top Tracks" viewDiscography = "Discography" + viewAllTracks = "All Tracks" ) func NewArtistPage(artistID string, cfg *backend.ArtistPageConfig, pool *util.WidgetPool, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager, contr *controller.Controller) *ArtistPage { activeView := 0 - if cfg.InitialView == viewTopTracks { + + switch cfg.InitialView { + case viewDiscography: + activeView = 0 + case viewTopTracks: activeView = 1 + case viewAllTracks: + activeView = 2 } + return newArtistPage(artistPageState{ artistID: artistID, cfg: cfg, @@ -102,7 +112,7 @@ func newArtistPage(state artistPageState) *ArtistPage { if img, ok := state.im.GetCachedArtistImage(state.artistID); ok { a.header.artistImage.SetImage(img, true /*tappable*/) } - viewToggle := widgets.NewToggleText(0, []string{lang.L("Discography"), lang.L("Top Tracks")}) + viewToggle := widgets.NewToggleText(0, []string{lang.L("Discography"), lang.L("Top Tracks"), lang.L("All Tracks")}) viewToggle.SetActivatedLabel(a.activeView) viewToggle.OnChanged = a.onViewChange a.sortButton = widgets.NewSortChooserButton(util.LocalizeSlice(discographySorts), func(selIdx int) { @@ -155,8 +165,14 @@ func (a *ArtistPage) Save() SavedPage { s := a.artistPageState if a.tracklistCtr != nil { tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist) - s.listScrollPos = tl.GetScrollOffset() - s.trackSort = tl.Sorting() + switch a.activeView { + case 1: + s.topListScrollPos = tl.GetScrollOffset() + s.topTrackSort = tl.Sorting() + case 2: + s.allListScrollPos = tl.GetScrollOffset() + s.allTrackSort = tl.Sorting() + } tl.Clear() a.pool.Release(util.WidgetTypeTracklist, tl) } @@ -195,7 +211,7 @@ func (g *ArtistPage) Scroll(scrollAmt float32) { g.albumGrid.ScrollToOffset(g.albumGrid.GetScrollOffset() + scrollAmt) } else if g.activeView == 0 && g.groupedReleases != nil { g.groupedReleases.ScrollToOffset(g.groupedReleases.GetScrollOffset() + scrollAmt) - } else if g.activeView == 1 && g.tracklistCtr != nil { + } else if (g.activeView == 1 || g.activeView == 2) && g.tracklistCtr != nil { tl := g.tracklistCtr.Objects[0].(*widgets.Tracklist) tl.ScrollBy(scrollAmt) } @@ -300,17 +316,13 @@ func (a *ArtistPage) load() { } a.artistInfo = artist a.header.Update(artist, a.im) - if a.activeView == 0 { - a.showAlbumGrid(false /*reSort*/) - } else { - go a.showTopTracks() - } }) info, err := a.mp.GetArtistInfo(a.artistID) if err != nil { log.Printf("Failed to get artist info: %s", err.Error()) } + a.onViewChange(a.activeView) fyne.Do(func() { if !a.disposed { a.header.UpdateInfo(info) @@ -329,9 +341,9 @@ func (a *ArtistPage) showAlbumGrid(reSort bool) { useGroupedReleases := a.artistInfo != nil && len(a.artistInfo.Albums) <= 50 && !allAlbums() if a.albumGrid == nil && a.groupedReleases == nil { + a.activeView = 0 if a.artistInfo == nil { // page not loaded yet or invalid artist - a.activeView = 0 // if page still loading, will show discography view first return } if useGroupedReleases { @@ -393,29 +405,20 @@ func (a *ArtistPage) showTopTracks() { a.container.Objects[0].Refresh() } - if a.tracklistCtr == nil { + if a.tracklistCtr == nil || a.activeView != 1 { + a.activeView = 1 if a.artistInfo == nil { // page not loaded yet or invalid artist - a.activeView = 1 // if page still loading, will show tracks view first return } - ts, err := a.mp.GetTopTracks(a.artistInfo.Artist, 20) - if err != nil { - log.Printf("error getting top songs: %s", err.Error()) - return - } - if a.disposed { - return - } - updated = true // mark that updatePage() will be called here + var tl *widgets.Tracklist fyne.Do(func() { - var tl *widgets.Tracklist if t := a.pool.Obtain(util.WidgetTypeTracklist); t != nil { tl = t.(*widgets.Tracklist) tl.Reset() - tl.SetTracks(ts) + tl.SetTracks([]*mediaprovider.Track{}) } else { - tl = widgets.NewTracklist(ts, a.im, false) + tl = widgets.NewTracklist([]*mediaprovider.Track{}, a.im, false) } tl.Options = widgets.TracklistOptions{AutoNumber: true} _, canRate := a.mp.(mediaprovider.SupportsRating) @@ -423,21 +426,106 @@ func (a *ArtistPage) showTopTracks() { tl.Options.DisableRating = !canRate tl.Options.DisableSharing = !canShare tl.SetVisibleColumns(a.cfg.TracklistColumns) - tl.SetSorting(a.trackSort) + tl.SetSorting(a.topTrackSort) tl.OnVisibleColumnsChanged = func(cols []string) { a.cfg.TracklistColumns = cols } - tl.SetNowPlaying(a.nowPlayingID) - a.contr.ConnectTracklistActions(tl) - if a.listScrollPos != 0 { - tl.ScrollToOffset(a.listScrollPos) - a.listScrollPos = 0 - } + a.tracklistCtr = container.New( &layout.CustomPaddedLayout{LeftPadding: 15, RightPadding: 15, BottomPadding: 10}, tl) updatePage() }) + + ts, err := a.mp.GetTopTracks(a.artistInfo.Artist, 20) + if err != nil { + log.Printf("error getting all songs: %s", err.Error()) + return + } + if a.disposed { + return + } + + updated = true // mark that updatePage() will be called here + fyne.Do(func() { + tl.SetTracks(ts) + + tl.SetNowPlaying(a.nowPlayingID) + a.contr.ConnectTracklistActions(tl) + if a.topListScrollPos != 0 { + tl.ScrollToOffset(a.topListScrollPos) + a.topListScrollPos = 0 + } + }) + + } + + if !updated { + fyne.Do(updatePage) + } +} + +// should be called asynchronously +func (a *ArtistPage) showAllTracks() { + updated := false + updatePage := func() { + a.sortButton.Hide() + a.container.Objects[0].(*fyne.Container).Objects[0] = a.tracklistCtr + a.container.Objects[0].Refresh() + } + + if a.tracklistCtr == nil || a.activeView != 2 { + a.activeView = 2 + if a.artistInfo == nil { + // page not loaded yet or invalid artist + return + } + var tl *widgets.Tracklist + fyne.Do(func() { + if t := a.pool.Obtain(util.WidgetTypeTracklist); t != nil { + tl = t.(*widgets.Tracklist) + tl.Reset() + tl.SetTracks([]*mediaprovider.Track{}) + } else { + tl = widgets.NewTracklist([]*mediaprovider.Track{}, a.im, false) + } + tl.Options = widgets.TracklistOptions{AutoNumber: true} + _, canRate := a.mp.(mediaprovider.SupportsRating) + _, canShare := a.mp.(mediaprovider.SupportsSharing) + tl.Options.DisableRating = !canRate + tl.Options.DisableSharing = !canShare + tl.SetVisibleColumns(a.cfg.TracklistColumns) + tl.SetSorting(a.allTrackSort) + tl.OnVisibleColumnsChanged = func(cols []string) { + a.cfg.TracklistColumns = cols + } + + a.tracklistCtr = container.New( + &layout.CustomPaddedLayout{LeftPadding: 15, RightPadding: 15, BottomPadding: 10}, + tl) + updatePage() + }) + + tracks, err := a.mp.GetArtistTracks(a.artistInfo.ID) + if err != nil { + log.Printf("error getting all songs: %s", err.Error()) + return + } + if a.disposed { + return + } + + updated = true // mark that updatePage() will be called here + fyne.Do(func() { + tl.SetTracks(tracks) + + tl.SetNowPlaying(a.nowPlayingID) + a.contr.ConnectTracklistActions(tl) + if a.allListScrollPos != 0 { + tl.ScrollToOffset(a.allListScrollPos) + a.allListScrollPos = 0 + } + }) } if !updated { @@ -446,18 +534,29 @@ func (a *ArtistPage) showTopTracks() { } func (a *ArtistPage) onViewChange(num int) { - if num == 0 { - a.showAlbumGrid(false /*reSort*/) - } else { - // needs to request info from server if first time, - // so call it asynchronously - go a.showTopTracks() + // save current data + if a.tracklistCtr != nil { + tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist) + switch a.activeView { + case 1: + a.topListScrollPos = tl.GetScrollOffset() + a.topTrackSort = tl.Sorting() + case 2: + a.allListScrollPos = tl.GetScrollOffset() + a.allTrackSort = tl.Sorting() + } } - a.activeView = num - if num == 1 { - a.cfg.InitialView = viewTopTracks - } else { + + switch num { + case 0: + a.showAlbumGrid(false /*reSort*/) a.cfg.InitialView = viewDiscography + case 1: + go a.showTopTracks() + a.cfg.InitialView = viewTopTracks + case 2: + go a.showAllTracks() + a.cfg.InitialView = viewAllTracks } } From f20e37fc5cd9b29ed7dbbae72ce976bf8e97001a Mon Sep 17 00:00:00 2001 From: zacaj Date: Sun, 18 Jan 2026 17:19:23 +0000 Subject: [PATCH 2/3] fix artist error logs --- ui/browsing/artistpage.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index e5fa289..33916cd 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -439,7 +439,7 @@ func (a *ArtistPage) showTopTracks() { ts, err := a.mp.GetTopTracks(a.artistInfo.Artist, 20) if err != nil { - log.Printf("error getting all songs: %s", err.Error()) + log.Printf("error getting top songs: %s", err.Error()) return } if a.disposed { @@ -508,7 +508,7 @@ func (a *ArtistPage) showAllTracks() { tracks, err := a.mp.GetArtistTracks(a.artistInfo.ID) if err != nil { - log.Printf("error getting all songs: %s", err.Error()) + log.Printf("error getting all songs for artist: %s", err.Error()) return } if a.disposed { From 13cb67284cc5a9af7cb1acde14e5b8b6f1c9b3e8 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 3 Feb 2026 08:40:30 -0800 Subject: [PATCH 3/3] some reworking --- ui/browsing/artistpage.go | 214 +++++++++++++++++++------------------- 1 file changed, 107 insertions(+), 107 deletions(-) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 33916cd..f2553b6 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -59,6 +59,9 @@ type ArtistPage struct { artistInfo *mediaprovider.ArtistWithAlbums + topTracks []*mediaprovider.Track + allTracks []*mediaprovider.Track + albumGrid *widgets.GridView groupedReleases *widgets.GroupedReleases tracklistCtr *fyne.Container @@ -157,6 +160,8 @@ func (a *ArtistPage) Route() controller.Route { } func (a *ArtistPage) Reload() { + a.topTracks = nil + a.allTracks = nil go a.load() } @@ -316,13 +321,13 @@ func (a *ArtistPage) load() { } a.artistInfo = artist a.header.Update(artist, a.im) + a.onViewChange(a.activeView) }) info, err := a.mp.GetArtistInfo(a.artistID) if err != nil { log.Printf("Failed to get artist info: %s", err.Error()) } - a.onViewChange(a.activeView) fyne.Do(func() { if !a.disposed { a.header.UpdateInfo(info) @@ -331,6 +336,11 @@ func (a *ArtistPage) load() { } func (a *ArtistPage) showAlbumGrid(reSort bool) { + if a.activeView == 0 && a.albumGrid != nil { + return // already showing album grid + } + a.activeView = 0 + allAlbums := func() bool { return slices.IndexFunc(a.artistInfo.Albums, func(al *mediaprovider.Album) bool { return al.ReleaseTypes&mediaprovider.ReleaseTypeCompilation > 0 || @@ -341,7 +351,6 @@ func (a *ArtistPage) showAlbumGrid(reSort bool) { useGroupedReleases := a.artistInfo != nil && len(a.artistInfo.Albums) <= 50 && !allAlbums() if a.albumGrid == nil && a.groupedReleases == nil { - a.activeView = 0 if a.artistInfo == nil { // page not loaded yet or invalid artist return @@ -396,141 +405,129 @@ func (a *ArtistPage) showAlbumGrid(reSort bool) { a.container.Objects[0].Refresh() } -// should be called asynchronously func (a *ArtistPage) showTopTracks() { - updated := false - updatePage := func() { - a.sortButton.Hide() - a.container.Objects[0].(*fyne.Container).Objects[0] = a.tracklistCtr - a.container.Objects[0].Refresh() + if a.activeView == 1 && a.tracklistCtr != nil { + return // already showing top tracks } - if a.tracklistCtr == nil || a.activeView != 1 { - a.activeView = 1 - if a.artistInfo == nil { - // page not loaded yet or invalid artist - return + var tl *widgets.Tracklist + if a.tracklistCtr == nil { + tl = a.obtainNewTracklist() + a.tracklistCtr = container.New( + &layout.CustomPaddedLayout{LeftPadding: 15, RightPadding: 15, BottomPadding: 10}, + tl) + a.contr.ConnectTracklistActions(tl) + } else { + tl = a.tracklistCtr.Objects[0].(*widgets.Tracklist) + tl.Clear() + } + a.activeView = 1 + tl.SetSorting(a.topTrackSort) + a.container.Objects[0].(*fyne.Container).Objects[0] = a.tracklistCtr + a.container.Objects[0].Refresh() + + updateTracklist := func(ts []*mediaprovider.Track) { + tl.SetTracks(ts) + tl.SetNowPlaying(a.nowPlayingID) + if a.topListScrollPos != 0 { + tl.ScrollToOffset(a.topListScrollPos) } - var tl *widgets.Tracklist - fyne.Do(func() { - if t := a.pool.Obtain(util.WidgetTypeTracklist); t != nil { - tl = t.(*widgets.Tracklist) - tl.Reset() - tl.SetTracks([]*mediaprovider.Track{}) - } else { - tl = widgets.NewTracklist([]*mediaprovider.Track{}, a.im, false) - } - tl.Options = widgets.TracklistOptions{AutoNumber: true} - _, canRate := a.mp.(mediaprovider.SupportsRating) - _, canShare := a.mp.(mediaprovider.SupportsSharing) - tl.Options.DisableRating = !canRate - tl.Options.DisableSharing = !canShare - tl.SetVisibleColumns(a.cfg.TracklistColumns) - tl.SetSorting(a.topTrackSort) - tl.OnVisibleColumnsChanged = func(cols []string) { - a.cfg.TracklistColumns = cols - } - - a.tracklistCtr = container.New( - &layout.CustomPaddedLayout{LeftPadding: 15, RightPadding: 15, BottomPadding: 10}, - tl) - updatePage() - }) + } + if a.topTracks != nil { + updateTracklist(a.topTracks) + return + } + tl.SetLoading(true) + go func() { ts, err := a.mp.GetTopTracks(a.artistInfo.Artist, 20) if err != nil { log.Printf("error getting top songs: %s", err.Error()) return } - if a.disposed { + if a.disposed || a.activeView != 1 { return } - updated = true // mark that updatePage() will be called here fyne.Do(func() { - tl.SetTracks(ts) - - tl.SetNowPlaying(a.nowPlayingID) - a.contr.ConnectTracklistActions(tl) - if a.topListScrollPos != 0 { - tl.ScrollToOffset(a.topListScrollPos) - a.topListScrollPos = 0 - } + a.topTracks = ts + tl.SetLoading(false) + updateTracklist(ts) }) - - } - - if !updated { - fyne.Do(updatePage) - } + }() } -// should be called asynchronously func (a *ArtistPage) showAllTracks() { - updated := false - updatePage := func() { - a.sortButton.Hide() - a.container.Objects[0].(*fyne.Container).Objects[0] = a.tracklistCtr - a.container.Objects[0].Refresh() + if a.activeView == 2 && a.tracklistCtr != nil { + return // already showing all tracks } - if a.tracklistCtr == nil || a.activeView != 2 { - a.activeView = 2 - if a.artistInfo == nil { - // page not loaded yet or invalid artist - return + var tl *widgets.Tracklist + if a.tracklistCtr == nil { + tl = a.obtainNewTracklist() + a.tracklistCtr = container.New( + &layout.CustomPaddedLayout{LeftPadding: 15, RightPadding: 15, BottomPadding: 10}, + tl) + a.contr.ConnectTracklistActions(tl) + } else { + tl = a.tracklistCtr.Objects[0].(*widgets.Tracklist) + tl.Clear() + } + a.activeView = 2 + tl.SetSorting(a.allTrackSort) + a.container.Objects[0].(*fyne.Container).Objects[0] = a.tracklistCtr + a.container.Objects[0].Refresh() + + updateTracklist := func(ts []*mediaprovider.Track) { + tl.SetTracks(ts) + tl.SetNowPlaying(a.nowPlayingID) + if a.allListScrollPos != 0 { + tl.ScrollToOffset(a.allListScrollPos) } - var tl *widgets.Tracklist - fyne.Do(func() { - if t := a.pool.Obtain(util.WidgetTypeTracklist); t != nil { - tl = t.(*widgets.Tracklist) - tl.Reset() - tl.SetTracks([]*mediaprovider.Track{}) - } else { - tl = widgets.NewTracklist([]*mediaprovider.Track{}, a.im, false) - } - tl.Options = widgets.TracklistOptions{AutoNumber: true} - _, canRate := a.mp.(mediaprovider.SupportsRating) - _, canShare := a.mp.(mediaprovider.SupportsSharing) - tl.Options.DisableRating = !canRate - tl.Options.DisableSharing = !canShare - tl.SetVisibleColumns(a.cfg.TracklistColumns) - tl.SetSorting(a.allTrackSort) - tl.OnVisibleColumnsChanged = func(cols []string) { - a.cfg.TracklistColumns = cols - } + } + if a.allTracks != nil { + updateTracklist(a.allTracks) + return + } - a.tracklistCtr = container.New( - &layout.CustomPaddedLayout{LeftPadding: 15, RightPadding: 15, BottomPadding: 10}, - tl) - updatePage() - }) - - tracks, err := a.mp.GetArtistTracks(a.artistInfo.ID) + tl.SetLoading(true) + go func() { + ts, err := a.mp.GetArtistTracks(a.artistID) if err != nil { - log.Printf("error getting all songs for artist: %s", err.Error()) + log.Printf("error getting all songs: %s", err.Error()) return } - if a.disposed { + if a.disposed || a.activeView != 2 { return } - updated = true // mark that updatePage() will be called here fyne.Do(func() { - tl.SetTracks(tracks) - - tl.SetNowPlaying(a.nowPlayingID) - a.contr.ConnectTracklistActions(tl) - if a.allListScrollPos != 0 { - tl.ScrollToOffset(a.allListScrollPos) - a.allListScrollPos = 0 - } + a.allTracks = ts + tl.SetLoading(false) + updateTracklist(ts) }) - } + }() +} - if !updated { - fyne.Do(updatePage) +func (a *ArtistPage) obtainNewTracklist() *widgets.Tracklist { + var tl *widgets.Tracklist + if t := a.pool.Obtain(util.WidgetTypeTracklist); t != nil { + tl = t.(*widgets.Tracklist) + tl.Reset() + tl.SetTracks([]*mediaprovider.Track{}) + } else { + tl = widgets.NewTracklist([]*mediaprovider.Track{}, a.im, false) } + tl.Options = widgets.TracklistOptions{AutoNumber: true} + _, canRate := a.mp.(mediaprovider.SupportsRating) + _, canShare := a.mp.(mediaprovider.SupportsSharing) + tl.Options.DisableRating = !canRate + tl.Options.DisableSharing = !canShare + tl.SetVisibleColumns(a.cfg.TracklistColumns) + tl.OnVisibleColumnsChanged = func(cols []string) { + a.cfg.TracklistColumns = cols + } + return tl } func (a *ArtistPage) onViewChange(num int) { @@ -550,12 +547,15 @@ func (a *ArtistPage) onViewChange(num int) { switch num { case 0: a.showAlbumGrid(false /*reSort*/) + a.sortButton.Show() a.cfg.InitialView = viewDiscography case 1: - go a.showTopTracks() + a.showTopTracks() + a.sortButton.Hide() a.cfg.InitialView = viewTopTracks case 2: - go a.showAllTracks() + a.showAllTracks() + a.sortButton.Hide() a.cfg.InitialView = viewAllTracks } }