Merge pull request #83 from dweymouth/feature/artist-top-tracks
Add Top Tracks view to Artist page
This commit is contained in:
@@ -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",
|
||||
|
||||
+124
-20
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"supersonic/backend"
|
||||
"supersonic/res"
|
||||
"supersonic/sharedutil"
|
||||
"supersonic/ui/controller"
|
||||
"supersonic/ui/layouts"
|
||||
"supersonic/ui/util"
|
||||
@@ -23,11 +24,14 @@ 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
|
||||
|
||||
cfg *backend.ArtistPageConfig
|
||||
pm *backend.PlaybackManager
|
||||
sm *backend.ServerManager
|
||||
im *backend.ImageManager
|
||||
contr *controller.Controller
|
||||
}
|
||||
|
||||
type ArtistPage struct {
|
||||
@@ -37,27 +41,63 @@ 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 {
|
||||
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, cfg *backend.ArtistPageConfig, 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,
|
||||
cfg: cfg,
|
||||
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,
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -67,10 +107,28 @@ 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
|
||||
}
|
||||
|
||||
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 +155,11 @@ 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()
|
||||
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())
|
||||
@@ -109,13 +167,59 @@ 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(a.cfg.TracklistColumns)
|
||||
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()
|
||||
}
|
||||
a.activeView = num
|
||||
if num == 1 {
|
||||
a.cfg.InitialView = "Top Tracks"
|
||||
} else {
|
||||
a.cfg.InitialView = "Discography"
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer {
|
||||
a.ExtendBaseWidget(a)
|
||||
return widget.NewSimpleRenderer(a.container)
|
||||
}
|
||||
|
||||
func (s *artistPageState) Restore() Page {
|
||||
return NewArtistPage(s.artistID, s.pm, s.sm, s.im, s.contr)
|
||||
return newArtistPage(s.artistID, s.cfg, s.pm, s.sm, s.im, s.contr, s.activeView)
|
||||
}
|
||||
|
||||
type ArtistPageHeader struct {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
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) 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)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
changed := t.activeLabelIdx != idx
|
||||
t.SetActivatedLabel(idx)
|
||||
if changed && t.OnChanged != nil {
|
||||
t.OnChanged(t.activeLabelIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ToggleText) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(t.container)
|
||||
}
|
||||
Reference in New Issue
Block a user