From 70ccf300144e573cd9cd59f0d26abcb2a09729c1 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 8 Mar 2023 19:52:09 -0800 Subject: [PATCH 1/4] add top tracks view to artist page --- ui/browsing/artistpage.go | 75 ++++++++++++++++++++++++++++++++++----- ui/widgets/toggletext.go | 67 ++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 ui/widgets/toggletext.go diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index d4f03e4..7bca923 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -7,6 +7,7 @@ import ( "strings" "supersonic/backend" "supersonic/res" + "supersonic/sharedutil" "supersonic/ui/controller" "supersonic/ui/layouts" "supersonic/ui/util" @@ -37,8 +38,11 @@ type ArtistPage struct { artistInfo *subsonic.ArtistID3 - header *ArtistPageHeader - container *fyne.Container + albumGrid *widgets.AlbumGrid + tracklistCtr *fyne.Container + nowPlayingID string + header *ArtistPageHeader + container *fyne.Container } func NewArtistPage(artistID string, pm *backend.PlaybackManager, sm *backend.ServerManager, im *backend.ImageManager, contr *controller.Controller) *ArtistPage { @@ -51,9 +55,17 @@ func NewArtistPage(artistID string, pm *backend.PlaybackManager, sm *backend.Ser }} a.ExtendBaseWidget(a) a.header = NewArtistPageHeader(a) + viewToggle := widgets.NewToggleText(0, []string{"Discography", "Top Tracks"}) + viewToggle.OnChanged = a.onViewChange + //line := canvas.NewLine(theme.TextColor()) + viewToggleRow := container.NewBorder(nil, nil, + container.NewHBox(&widgets.HSpace{Width: 5}, viewToggle), nil, + layout.NewSpacer(), + ) a.container = container.NewBorder( container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 15, PadBottom: 10}, a.header), - nil, nil, nil, layout.NewSpacer()) + nil, nil, nil, + container.NewBorder(viewToggleRow, nil, nil, nil, layout.NewSpacer())) go a.load() return a } @@ -71,6 +83,17 @@ func (a *ArtistPage) Save() SavedPage { return &s } +var _ CanShowNowPlaying = (*ArtistPage)(nil) + +func (a *ArtistPage) OnSongChange(track *subsonic.Child, lastScrobbledIfAny *subsonic.Child) { + a.nowPlayingID = sharedutil.TrackIDOrEmptyStr(track) + if a.tracklistCtr != nil { + tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist) + tl.SetNowPlaying(a.nowPlayingID) + tl.IncrementPlayCount(sharedutil.TrackIDOrEmptyStr(lastScrobbledIfAny)) + } +} + func (a *ArtistPage) onPlayAlbum(albumID string) { a.pm.PlayAlbum(albumID, 0) } @@ -97,11 +120,7 @@ func (a *ArtistPage) load() { } a.artistInfo = artist a.header.Update(artist) - ag := widgets.NewFixedAlbumGrid(artist.Album, a.im, true /*showYear*/) - ag.OnPlayAlbum = a.onPlayAlbum - ag.OnShowAlbumPage = a.onShowAlbumPage - a.container.Objects[0] = ag - a.container.Refresh() + a.showAlbumGrid() info, err := a.sm.Server.GetArtistInfo2(a.artistID, nil) if err != nil { log.Printf("Failed to get artist info: %s", err.Error()) @@ -109,6 +128,46 @@ func (a *ArtistPage) load() { a.header.UpdateInfo(info) } +func (a *ArtistPage) showAlbumGrid() { + if a.albumGrid == nil { + a.albumGrid = widgets.NewFixedAlbumGrid(a.artistInfo.Album, a.im, true /*showYear*/) + a.albumGrid.OnPlayAlbum = a.onPlayAlbum + a.albumGrid.OnShowAlbumPage = a.onShowAlbumPage + } + a.container.Objects[0].(*fyne.Container).Objects[0] = a.albumGrid + a.container.Objects[0].Refresh() +} + +func (a *ArtistPage) showTopTracks() { + if a.tracklistCtr == nil { + ts, err := a.sm.Server.GetTopSongs(a.artistInfo.Name, map[string]string{"count": "20"}) + if err != nil { + log.Printf("error getting top songs: %s", err.Error()) + return + } + tl := widgets.NewTracklist(ts) + tl.AutoNumber = true + tl.SetVisibleColumns([]string{"Album", "Time", "Year", "Plays"}) + tl.SetNowPlaying(a.nowPlayingID) + a.contr.ConnectTracklistActions(tl) + a.tracklistCtr = container.New( + &layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadBottom: 10}, + tl) + } + a.container.Objects[0].(*fyne.Container).Objects[0] = a.tracklistCtr + a.container.Objects[0].Refresh() +} + +func (a *ArtistPage) onViewChange(num int) { + if num == 0 { + a.showAlbumGrid() + } else { + // needs to request info from server if first time, + // so call it asynchronously + go a.showTopTracks() + } +} + func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer { a.ExtendBaseWidget(a) return widget.NewSimpleRenderer(a.container) diff --git a/ui/widgets/toggletext.go b/ui/widgets/toggletext.go new file mode 100644 index 0000000..999c6c1 --- /dev/null +++ b/ui/widgets/toggletext.go @@ -0,0 +1,67 @@ +package widgets + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" +) + +type ToggleText struct { + widget.BaseWidget + + OnChanged func(int) + + labels []string + activeLabelIdx int + container *fyne.Container +} + +func NewToggleText(activeLblIdx int, labels []string) *ToggleText { + t := &ToggleText{labels: labels, activeLabelIdx: activeLblIdx} + t.ExtendBaseWidget(t) + t.container = container.NewHBox() + for i, lbl := range labels { + if i == activeLblIdx { + t.container.Add(t.newBoldRichText(lbl)) + } else { + hl := widget.NewHyperlink(lbl, nil) + hl.OnTapped = t.buildOnTapped(i) + t.container.Add(hl) + } + } + return t +} + +func (t *ToggleText) buildOnTapped(i int) func() { + return func() { + t.onActivated(i) + } +} + +func (t *ToggleText) newBoldRichText(text string) *widget.RichText { + return widget.NewRichText(&widget.TextSegment{ + Text: text, + Style: widget.RichTextStyle{TextStyle: fyne.TextStyle{Bold: true}}, + }) +} + +func (t *ToggleText) onActivated(idx int) { + if idx == t.activeLabelIdx { + return + } + // update old label to hyperlink + hl := widget.NewHyperlink(t.labels[t.activeLabelIdx], nil) + hl.OnTapped = t.buildOnTapped(t.activeLabelIdx) + t.container.Objects[t.activeLabelIdx] = hl + // update activated label to bold text + t.container.Objects[idx] = t.newBoldRichText(t.labels[idx]) + t.activeLabelIdx = idx + t.Refresh() + if t.OnChanged != nil { + t.OnChanged(t.activeLabelIdx) + } +} + +func (t *ToggleText) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(t.container) +} From 4abb3843a25f6169c27652b66d8bd9581409e237 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 8 Mar 2023 20:51:56 -0800 Subject: [PATCH 2/4] save active artist page view in history --- ui/browsing/artistpage.go | 37 +++++++++++++++++++++++++------------ ui/widgets/toggletext.go | 29 +++++++++++++++++------------ 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 7bca923..0d26213 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -24,11 +24,13 @@ import ( var _ fyne.Widget = (*ArtistPage)(nil) type artistPageState struct { - artistID string - pm *backend.PlaybackManager - sm *backend.ServerManager - im *backend.ImageManager - contr *controller.Controller + artistID string + activeView int + + pm *backend.PlaybackManager + sm *backend.ServerManager + im *backend.ImageManager + contr *controller.Controller } type ArtistPage struct { @@ -46,16 +48,22 @@ type ArtistPage struct { } func NewArtistPage(artistID string, pm *backend.PlaybackManager, sm *backend.ServerManager, im *backend.ImageManager, contr *controller.Controller) *ArtistPage { + return newArtistPage(artistID, pm, sm, im, contr, 0) +} + +func newArtistPage(artistID string, pm *backend.PlaybackManager, sm *backend.ServerManager, im *backend.ImageManager, contr *controller.Controller, activeView int) *ArtistPage { a := &ArtistPage{artistPageState: artistPageState{ - artistID: artistID, - pm: pm, - sm: sm, - im: im, - contr: contr, + artistID: artistID, + pm: pm, + sm: sm, + im: im, + contr: contr, + activeView: activeView, }} a.ExtendBaseWidget(a) a.header = NewArtistPageHeader(a) viewToggle := widgets.NewToggleText(0, []string{"Discography", "Top Tracks"}) + viewToggle.SetActivatedLabel(a.activeView) viewToggle.OnChanged = a.onViewChange //line := canvas.NewLine(theme.TextColor()) viewToggleRow := container.NewBorder(nil, nil, @@ -120,7 +128,11 @@ func (a *ArtistPage) load() { } a.artistInfo = artist a.header.Update(artist) - a.showAlbumGrid() + if a.activeView == 0 { + a.showAlbumGrid() + } else { + a.showTopTracks() + } info, err := a.sm.Server.GetArtistInfo2(a.artistID, nil) if err != nil { log.Printf("Failed to get artist info: %s", err.Error()) @@ -166,6 +178,7 @@ func (a *ArtistPage) onViewChange(num int) { // so call it asynchronously go a.showTopTracks() } + a.activeView = num } func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer { @@ -174,7 +187,7 @@ func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer { } func (s *artistPageState) Restore() Page { - return NewArtistPage(s.artistID, s.pm, s.sm, s.im, s.contr) + return newArtistPage(s.artistID, s.pm, s.sm, s.im, s.contr, s.activeView) } type ArtistPageHeader struct { diff --git a/ui/widgets/toggletext.go b/ui/widgets/toggletext.go index 999c6c1..95d46e2 100644 --- a/ui/widgets/toggletext.go +++ b/ui/widgets/toggletext.go @@ -32,6 +32,20 @@ func NewToggleText(activeLblIdx int, labels []string) *ToggleText { return t } +func (t *ToggleText) SetActivatedLabel(idx int) { + changed := t.activeLabelIdx != idx + // update old label to hyperlink + hl := widget.NewHyperlink(t.labels[t.activeLabelIdx], nil) + hl.OnTapped = t.buildOnTapped(t.activeLabelIdx) + t.container.Objects[t.activeLabelIdx] = hl + // update activated label to bold text + t.container.Objects[idx] = t.newBoldRichText(t.labels[idx]) + t.activeLabelIdx = idx + if changed { + t.Refresh() + } +} + func (t *ToggleText) buildOnTapped(i int) func() { return func() { t.onActivated(i) @@ -46,18 +60,9 @@ func (t *ToggleText) newBoldRichText(text string) *widget.RichText { } func (t *ToggleText) onActivated(idx int) { - if idx == t.activeLabelIdx { - return - } - // update old label to hyperlink - hl := widget.NewHyperlink(t.labels[t.activeLabelIdx], nil) - hl.OnTapped = t.buildOnTapped(t.activeLabelIdx) - t.container.Objects[t.activeLabelIdx] = hl - // update activated label to bold text - t.container.Objects[idx] = t.newBoldRichText(t.labels[idx]) - t.activeLabelIdx = idx - t.Refresh() - if t.OnChanged != nil { + changed := t.activeLabelIdx != idx + t.SetActivatedLabel(idx) + if changed && t.OnChanged != nil { t.OnChanged(t.activeLabelIdx) } } From a6aae2b8a862a71e981553e1c72c49ea3fbdfa28 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 9 Mar 2023 09:04:38 -0800 Subject: [PATCH 3/4] save artist page view settings in config --- backend/config.go | 10 ++++++++++ ui/browsing/artistpage.go | 28 +++++++++++++++++++++++----- ui/browsing/router.go | 2 +- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/backend/config.go b/backend/config.go index ecfce24..8de416c 100644 --- a/backend/config.go +++ b/backend/config.go @@ -29,6 +29,11 @@ type AlbumsPageConfig struct { SortOrder string } +type ArtistPageConfig struct { + InitialView string + TracklistColumns []string +} + type FavoritesPageConfig struct { InitialView string TracklistColumns []string @@ -51,6 +56,7 @@ type Config struct { Servers []*ServerConfig AlbumPage AlbumPageConfig AlbumsPage AlbumsPageConfig + ArtistPage ArtistPageConfig FavoritesPage FavoritesPageConfig NowPlayingPage NowPlayingPageConfig PlaylistPage PlaylistPageConfig @@ -69,6 +75,10 @@ func DefaultConfig() *Config { AlbumsPage: AlbumsPageConfig{ SortOrder: string(AlbumSortRecentlyAdded), }, + ArtistPage: ArtistPageConfig{ + InitialView: "Discography", + TracklistColumns: []string{"Album", "Time", "Plays", "Favorite"}, + }, FavoritesPage: FavoritesPageConfig{ TracklistColumns: []string{"Artist", "Album", "Time", "Plays"}, InitialView: "Albums", diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 0d26213..e2ab418 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -27,6 +27,7 @@ type artistPageState struct { artistID string activeView int + cfg *backend.ArtistPageConfig pm *backend.PlaybackManager sm *backend.ServerManager im *backend.ImageManager @@ -47,13 +48,18 @@ type ArtistPage struct { container *fyne.Container } -func NewArtistPage(artistID string, pm *backend.PlaybackManager, sm *backend.ServerManager, im *backend.ImageManager, contr *controller.Controller) *ArtistPage { - return newArtistPage(artistID, pm, sm, im, contr, 0) +func NewArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.PlaybackManager, sm *backend.ServerManager, im *backend.ImageManager, contr *controller.Controller) *ArtistPage { + activeView := 0 + if cfg.InitialView == "Top Tracks" { + activeView = 1 + } + return newArtistPage(artistID, cfg, pm, sm, im, contr, activeView) } -func newArtistPage(artistID string, pm *backend.PlaybackManager, sm *backend.ServerManager, im *backend.ImageManager, contr *controller.Controller, activeView int) *ArtistPage { +func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.PlaybackManager, sm *backend.ServerManager, im *backend.ImageManager, contr *controller.Controller, activeView int) *ArtistPage { a := &ArtistPage{artistPageState: artistPageState{ artistID: artistID, + cfg: cfg, pm: pm, sm: sm, im: im, @@ -87,6 +93,13 @@ func (a *ArtistPage) Reload() { } func (a *ArtistPage) Save() SavedPage { + // TODO: find a better place to update the tracklist columns preference + // If user changes columns but doesn't navigate to another page, + // we won't be persisting the change + if a.tracklistCtr != nil { + tl := a.tracklistCtr.Objects[0].(*widgets.Tracklist) + a.cfg.TracklistColumns = tl.VisibleColumns() + } s := a.artistPageState return &s } @@ -159,7 +172,7 @@ func (a *ArtistPage) showTopTracks() { } tl := widgets.NewTracklist(ts) tl.AutoNumber = true - tl.SetVisibleColumns([]string{"Album", "Time", "Year", "Plays"}) + tl.SetVisibleColumns(a.cfg.TracklistColumns) tl.SetNowPlaying(a.nowPlayingID) a.contr.ConnectTracklistActions(tl) a.tracklistCtr = container.New( @@ -179,6 +192,11 @@ func (a *ArtistPage) onViewChange(num int) { go a.showTopTracks() } a.activeView = num + if num == 1 { + a.cfg.InitialView = "Top Tracks" + } else { + a.cfg.InitialView = "Discography" + } } func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer { @@ -187,7 +205,7 @@ func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer { } func (s *artistPageState) Restore() Page { - return newArtistPage(s.artistID, s.pm, s.sm, s.im, s.contr, s.activeView) + return newArtistPage(s.artistID, s.cfg, s.pm, s.sm, s.im, s.contr, s.activeView) } type ArtistPageHeader struct { diff --git a/ui/browsing/router.go b/ui/browsing/router.go index 256e03b..70a34f6 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -31,7 +31,7 @@ func (r Router) CreatePage(rte controller.Route) Page { case controller.Albums: return NewAlbumsPage(&r.App.Config.AlbumsPage, r.Controller, r.App.PlaybackManager, r.App.LibraryManager, r.App.ImageManager) case controller.Artist: - return NewArtistPage(rte.Arg, r.App.PlaybackManager, r.App.ServerManager, r.App.ImageManager, r.Controller) + return NewArtistPage(rte.Arg, &r.App.Config.ArtistPage, r.App.PlaybackManager, r.App.ServerManager, r.App.ImageManager, r.Controller) case controller.Artists: return NewArtistsGenresPage(false, r.Controller, r.App.ServerManager) case controller.Favorites: From c06bdc09967f2d82c7c08aa3fe874025cbd2a1d6 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 9 Mar 2023 17:47:36 -0800 Subject: [PATCH 4/4] connect select all action to top tracks view when visible --- ui/browsing/artistpage.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index e2ab418..2249e17 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -84,6 +84,20 @@ func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.P return a } +func (a *ArtistPage) Tapped(*fyne.PointEvent) { + if a.tracklistCtr != nil { + a.tracklistCtr.Objects[0].(*widgets.Tracklist).UnselectAll() + } +} + +var _ CanSelectAll = (*ArtistPage)(nil) + +func (a *ArtistPage) SelectAll() { + if a.activeView == 1 && a.tracklistCtr != nil { + a.tracklistCtr.Objects[0].(*widgets.Tracklist).SelectAll() + } +} + func (a *ArtistPage) Route() controller.Route { return controller.ArtistRoute(a.artistID) }