From 4f73121d9b16b2e0240adeac2384b39c2955c896 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 24 Jan 2023 18:18:11 -0800 Subject: [PATCH 1/6] adding playlists page --- ui/browsing/playlistspage.go | 173 +++++++++++++++++++++++++++++++++++ ui/browsing/router.go | 6 ++ ui/mainwindow.go | 3 + 3 files changed, 182 insertions(+) create mode 100644 ui/browsing/playlistspage.go diff --git a/ui/browsing/playlistspage.go b/ui/browsing/playlistspage.go new file mode 100644 index 0000000..594a1d9 --- /dev/null +++ b/ui/browsing/playlistspage.go @@ -0,0 +1,173 @@ +package browsing + +import ( + "log" + "strconv" + "supersonic/backend" + "supersonic/ui/layouts" + "supersonic/ui/widgets" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "github.com/dweymouth/go-subsonic" +) + +type PlaylistsPage struct { + widget.BaseWidget + + sm *backend.ServerManager + nav func(Route) + titleDisp *widget.RichText + container *fyne.Container + list *PlaylistList +} + +func NewPlaylistsPage(sm *backend.ServerManager, nav func(Route)) *PlaylistsPage { + a := &PlaylistsPage{ + sm: sm, + nav: nav, + titleDisp: widget.NewRichTextWithText("Playlists"), + } + a.ExtendBaseWidget(a) + a.titleDisp.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameHeadingText + a.list = NewPlaylistList() + a.list.OnNavTo = func(id string) { + log.Println("opening playlist", id) + //nav(PlaylistRoute(id)) + } + a.buildContainer() + go a.loadAsync() + return a +} + +func (a *PlaylistsPage) loadAsync() { + playlists, err := a.sm.Server.GetPlaylists(nil) + if err != nil { + log.Printf("error loading playlists: %v", err.Error()) + } + a.list.Playlists = playlists + a.list.Refresh() +} + +func (a *PlaylistsPage) Route() Route { + return PlaylistsRoute() +} + +func (a *PlaylistsPage) Reload() { + go a.loadAsync() +} + +func (a *PlaylistsPage) Save() SavedPage { + return &savedPlaylistsPage{ + sm: a.sm, + nav: a.nav, + } +} + +type savedPlaylistsPage struct { + sm *backend.ServerManager + nav func(Route) +} + +func (s *savedPlaylistsPage) Restore() Page { + return NewPlaylistsPage(s.sm, s.nav) +} + +func (a *PlaylistsPage) buildContainer() { + a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15}, + container.NewBorder(a.titleDisp, nil, nil, nil, a.list)) +} + +func (a *PlaylistsPage) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(a.container) +} + +type PlaylistList struct { + widget.BaseWidget + + Playlists []*subsonic.Playlist + OnNavTo func(string) + + columnsLayout *layouts.ColumnsLayout + header *widgets.ListHeader + list *widget.List + container *fyne.Container +} + +func NewPlaylistList() *PlaylistList { + a := &PlaylistList{ + columnsLayout: layouts.NewColumnsLayout([]float32{-1, -1, 200, 125}), + } + a.header = widgets.NewListHeader([]string{"Name", "Description", "Owner", "Track Count"}, a.columnsLayout) + a.list = widget.NewList( + func() int { + return len(a.Playlists) + }, + func() fyne.CanvasObject { + r := NewPlaylistListRow(a.columnsLayout) + r.OnTapped = func() { a.onRowTapped(r.ID) } + return r + }, + func(id widget.ListItemID, item fyne.CanvasObject) { + row := item.(*PlaylistListRow) + row.ID = a.Playlists[id].ID + row.nameLabel.Text = a.Playlists[id].Name + row.descrptionLabel.Text = a.Playlists[id].Name + row.ownerLabel.Text = a.Playlists[id].Owner + row.trackCountLabel.Text = strconv.Itoa(a.Playlists[id].SongCount) + row.Refresh() + }, + ) + a.container = container.NewBorder(a.header, nil, nil, nil, a.list) + a.ExtendBaseWidget(a) + return a +} + +func (p *PlaylistList) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(p.container) +} + +func (p *PlaylistList) onRowTapped(id string) { + if p.OnNavTo != nil { + p.OnNavTo(id) + } +} + +type PlaylistListRow struct { + widget.BaseWidget + + ID string + OnTapped func() + + nameLabel *widget.Label + descrptionLabel *widget.Label + ownerLabel *widget.Label + trackCountLabel *widget.Label + + container *fyne.Container +} + +func NewPlaylistListRow(layout *layouts.ColumnsLayout) *PlaylistListRow { + a := &PlaylistListRow{ + nameLabel: widget.NewLabel(""), + descrptionLabel: widget.NewLabel(""), + ownerLabel: widget.NewLabel(""), + trackCountLabel: widget.NewLabel(""), + } + a.ownerLabel.Wrapping = fyne.TextTruncate + a.container = container.New(layout, a.nameLabel, a.descrptionLabel, a.ownerLabel, a.trackCountLabel) + a.ExtendBaseWidget(a) + return a +} + +func (a *PlaylistListRow) Tapped(*fyne.PointEvent) { + if a.OnTapped != nil { + a.OnTapped() + } +} + +func (a *PlaylistListRow) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(a.container) +} diff --git a/ui/browsing/router.go b/ui/browsing/router.go index dc7b8e0..31aef17 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -51,6 +51,10 @@ func GenresRoute() Route { return Route{Page: Genres} } +func PlaylistsRoute() Route { + return Route{Page: Playlists} +} + func ArtistsRoute() Route { return Route{Page: Artists} } @@ -105,6 +109,8 @@ func (r Router) CreatePage(rte Route) Page { return NewGenrePage(rte.Arg, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute) case Genres: return NewArtistsGenresPage(true, r.App.ServerManager, r.OpenRoute) + case Playlists: + return NewPlaylistsPage(r.App.ServerManager, r.OpenRoute) } return nil } diff --git a/ui/mainwindow.go b/ui/mainwindow.go index e47d20a..adcc4c9 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -87,6 +87,9 @@ func (m *MainWindow) addNavigationButtons() { m.BrowsingPane.AddNavigationButton(res.ResTheatermasksInvertPng, func() { m.Router.OpenRoute(browsing.GenresRoute()) }) + m.BrowsingPane.AddNavigationButton(res.ResPlaylistInvertPng, func() { + m.Router.OpenRoute(browsing.PlaylistsRoute()) + }) } func (m *MainWindow) addShortcuts() { From c4a7c6c95cec80bc5fde057f47ee419121c1b9f7 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 25 Jan 2023 08:53:23 -0800 Subject: [PATCH 2/6] add rudimentary playlist page --- backend/playbackmanager.go | 25 ++++- ui/browsing/playlistpage.go | 175 +++++++++++++++++++++++++++++++++++ ui/browsing/playlistspage.go | 3 +- ui/browsing/router.go | 5 + 4 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 ui/browsing/playlistpage.go diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index f2426ac..826f6c5 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -108,12 +108,25 @@ func (p *PlaybackManager) LoadAlbum(albumID string, appendToQueue bool) error { if err != nil { return err } + return p.LoadTracks(album.Song, appendToQueue) +} + +// Loads the specified playlist into the play queue. +func (p *PlaybackManager) LoadPlaylist(playlistID string, appendToQueue bool) error { + playlist, err := p.sm.Server.GetPlaylist(playlistID) + if err != nil { + return err + } + return p.LoadTracks(playlist.Entry, appendToQueue) +} + +func (p *PlaybackManager) LoadTracks(tracks []*subsonic.Child, appendToQueue bool) error { if !appendToQueue { p.player.Stop() p.nowPlayingIdx = 0 p.playQueue = nil } - for _, song := range album.Song { + for _, song := range tracks { url, err := p.sm.Server.GetStreamURL(song.ID, map[string]string{}) if err != nil { return err @@ -134,6 +147,16 @@ func (p *PlaybackManager) PlayAlbum(albumID string, firstTrack int) error { return p.player.PlayTrackAt(firstTrack) } +func (p *PlaybackManager) PlayPlaylist(playlistID string, firstTrack int) error { + if err := p.LoadPlaylist(playlistID, false); err != nil { + return err + } + if firstTrack <= 0 { + return p.player.PlayFromBeginning() + } + return p.player.PlayTrackAt(firstTrack) +} + func (p *PlaybackManager) checkScrobble(playDur time.Duration) { if playDur.Seconds() < 0.1 || p.curTrackTime < 0.1 { return diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go new file mode 100644 index 0000000..d49f8e3 --- /dev/null +++ b/ui/browsing/playlistpage.go @@ -0,0 +1,175 @@ +package browsing + +import ( + "fmt" + "log" + "supersonic/backend" + "supersonic/ui/layouts" + "supersonic/ui/util" + "supersonic/ui/widgets" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "github.com/dweymouth/go-subsonic" +) + +type PlaylistPage struct { + widget.BaseWidget + + playlistID string + sm *backend.ServerManager + pm *backend.PlaybackManager + nav func(Route) + header *PlaylistPageHeader + tracklist *widgets.Tracklist + nowPlayingID string + container *fyne.Container + popUpProvider PopUpProvider +} + +func NewPlaylistPage( + playlistID string, + sm *backend.ServerManager, + pm *backend.PlaybackManager, + nav func(Route), +) *PlaylistPage { + a := &PlaylistPage{playlistID: playlistID, sm: sm, pm: pm} + a.ExtendBaseWidget(a) + a.header = NewPlaylistPageHeader(a) + a.tracklist = widgets.NewTracklist(nil) + a.tracklist.OnPlayTrackAt = a.onPlayTrackAt + a.container = container.NewBorder( + container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 15, PadBottom: 10}, a.header), + nil, nil, nil, container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadBottom: 15}, a.tracklist)) + a.loadAsync() + return a +} + +func (a *PlaylistPage) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(a.container) +} + +func (a *PlaylistPage) Save() SavedPage { + return &savedPlaylistPage{ + playlistID: a.playlistID, + sm: a.sm, + pm: a.pm, + nav: a.nav, + } +} + +func (a *PlaylistPage) Route() Route { + return AlbumRoute(a.playlistID) +} + +func (a *PlaylistPage) OnSongChange(song *subsonic.Child) { + if song == nil { + a.nowPlayingID = "" + } else { + a.nowPlayingID = song.ID + } + a.tracklist.SetNowPlaying(a.nowPlayingID) +} + +func (a *PlaylistPage) Reload() { + a.loadAsync() +} + +func (a *PlaylistPage) onPlayTrackAt(tracknum int) { + a.pm.PlayPlaylist(a.playlistID, tracknum) +} + +func (a *PlaylistPage) loadAsync() { + go func() { + playlist, err := a.sm.Server.GetPlaylist(a.playlistID) + if err != nil { + log.Printf("Failed to get playlist: %s", err.Error()) + return + } + a.header.Update(playlist) + a.tracklist.Tracks = playlist.Entry + a.tracklist.SetNowPlaying(a.nowPlayingID) + }() +} + +type PlaylistPageHeader struct { + widget.BaseWidget + + page *PlaylistPage + + titleLabel *widget.RichText + descriptionLabel *widget.Label + createdAtLabel *widget.Label + ownerLabel *widget.Label + trackTimeLabel *widget.Label + + playButton *widget.Button + + container *fyne.Container +} + +func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { + a := &PlaylistPageHeader{page: page} + a.ExtendBaseWidget(a) + + a.titleLabel = widget.NewRichTextWithText("") + a.titleLabel.Wrapping = fyne.TextTruncate + a.titleLabel.Segments[0].(*widget.TextSegment).Style = widget.RichTextStyle{ + SizeName: theme.SizeNameHeadingText, + } + a.descriptionLabel = widget.NewLabel("") + a.ownerLabel = widget.NewLabel("") + a.createdAtLabel = widget.NewLabel("") + a.trackTimeLabel = widget.NewLabel("") + a.playButton = widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() { + page.onPlayTrackAt(0) + }) + + // Todo: there's got to be a way to make this less convoluted. Custom layout? + a.container = container.NewVBox(a.titleLabel, container.New(&layouts.VboxCustomPadding{ExtraPad: -10}, + a.descriptionLabel, + a.ownerLabel, + a.trackTimeLabel, + a.createdAtLabel), + container.NewHBox(a.playButton), + ) + return a +} + +func (a *PlaylistPageHeader) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(a.container) +} + +func (a *PlaylistPageHeader) Update(playlist *subsonic.Playlist) { + a.titleLabel.Segments[0].(*widget.TextSegment).Text = playlist.Name + a.descriptionLabel.SetText(playlist.Comment) + a.ownerLabel.SetText(a.formatPlaylistOwnerStr(playlist)) + a.trackTimeLabel.SetText(a.formatPlaylistTrackTimeStr(playlist)) + a.createdAtLabel.SetText("created at TODO") + a.Refresh() +} + +func (a *PlaylistPageHeader) formatPlaylistOwnerStr(p *subsonic.Playlist) string { + pubPriv := "Public" + if !p.Public { + pubPriv = "Private" + } + return fmt.Sprintf("%s playlist by %s", pubPriv, p.Owner) +} + +func (a *PlaylistPageHeader) formatPlaylistTrackTimeStr(p *subsonic.Playlist) string { + return fmt.Sprintf("%d tracks, %s", p.SongCount, util.SecondsToTimeString(float64(p.Duration))) +} + +type savedPlaylistPage struct { + playlistID string + sm *backend.ServerManager + pm *backend.PlaybackManager + nav func(Route) +} + +func (s *savedPlaylistPage) Restore() Page { + return NewPlaylistPage(s.playlistID, s.sm, s.pm, s.nav) +} diff --git a/ui/browsing/playlistspage.go b/ui/browsing/playlistspage.go index 594a1d9..05d110b 100644 --- a/ui/browsing/playlistspage.go +++ b/ui/browsing/playlistspage.go @@ -34,8 +34,7 @@ func NewPlaylistsPage(sm *backend.ServerManager, nav func(Route)) *PlaylistsPage a.titleDisp.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameHeadingText a.list = NewPlaylistList() a.list.OnNavTo = func(id string) { - log.Println("opening playlist", id) - //nav(PlaylistRoute(id)) + nav(PlaylistRoute(id)) } a.buildContainer() go a.loadAsync() diff --git a/ui/browsing/router.go b/ui/browsing/router.go index 31aef17..9d4cb03 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -51,6 +51,9 @@ func GenresRoute() Route { return Route{Page: Genres} } +func PlaylistRoute(id string) Route { + return Route{Page: Playlist, Arg: id} +} func PlaylistsRoute() Route { return Route{Page: Playlists} } @@ -109,6 +112,8 @@ func (r Router) CreatePage(rte Route) Page { return NewGenrePage(rte.Arg, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute) case Genres: return NewArtistsGenresPage(true, r.App.ServerManager, r.OpenRoute) + case Playlist: + return NewPlaylistPage(rte.Arg, r.App.ServerManager, r.App.PlaybackManager, r.OpenRoute) case Playlists: return NewPlaylistsPage(r.App.ServerManager, r.OpenRoute) } From 240f37a882ccbe81d6bdece0e07792388c72a440 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 26 Jan 2023 09:39:13 -0800 Subject: [PATCH 3/6] auto-number playlist track list in sequential order --- ui/browsing/playlistpage.go | 1 + ui/widgets/tracklist.go | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index d49f8e3..1d065fc 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -39,6 +39,7 @@ func NewPlaylistPage( a.ExtendBaseWidget(a) a.header = NewPlaylistPageHeader(a) a.tracklist = widgets.NewTracklist(nil) + a.tracklist.AutoNumber = true a.tracklist.OnPlayTrackAt = a.onPlayTrackAt a.container = container.NewBorder( container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 15, PadBottom: 10}, a.header), diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index cbb99db..63aabc5 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -45,14 +45,18 @@ func NewTrackRow(layout *layouts.ColumnsLayout) *TrackRow { return t } -func (t *TrackRow) Update(tr *subsonic.Child, isPlaying bool) { +func (t *TrackRow) Update(tr *subsonic.Child, isPlaying bool, rowNum int) { if tr.ID == t.prevTrackID && isPlaying == t.prevIsPlaying { return } t.prevTrackID = t.trackID t.prevIsPlaying = isPlaying t.trackID = tr.ID - t.num.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(tr.Track) + + if rowNum < 0 { + rowNum = tr.Track + } + t.num.Segments[0].(*widget.TextSegment).Text = strconv.Itoa(rowNum) t.name.Segments[0].(*widget.TextSegment).Text = tr.Title t.artist.Segments[0].(*widget.TextSegment).Text = tr.Artist t.dur.Segments[0].(*widget.TextSegment).Text = util.SecondsToTimeString(float64(tr.Duration)) @@ -79,6 +83,7 @@ type Tracklist struct { widget.BaseWidget Tracks []*subsonic.Child + AutoNumber bool OnPlayTrackAt func(int) nowPlayingIdx int @@ -99,7 +104,11 @@ func NewTracklist(tracks []*subsonic.Child) *Tracklist { func(itemID widget.ListItemID, item fyne.CanvasObject) { tr := item.(*TrackRow) tr.OnDoubleTapped = func() { t.onPlayTrackAt(itemID) } - tr.Update(t.Tracks[itemID], itemID == t.nowPlayingIdx) + i := itemID + 1 + if !t.AutoNumber { + i = -1 // signal that we want to use the track num. + } + tr.Update(t.Tracks[itemID], itemID == t.nowPlayingIdx, i) }) t.container = container.NewBorder(t.hdr, nil, nil, nil, t.list) return t From 80c72240396aee3926e4f2104f5ad3c2c12544ec Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 26 Jan 2023 09:41:21 -0800 Subject: [PATCH 4/6] remove created at label for now --- ui/browsing/playlistpage.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 1d065fc..dbdadc0 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -132,8 +132,7 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { a.container = container.NewVBox(a.titleLabel, container.New(&layouts.VboxCustomPadding{ExtraPad: -10}, a.descriptionLabel, a.ownerLabel, - a.trackTimeLabel, - a.createdAtLabel), + a.trackTimeLabel), container.NewHBox(a.playButton), ) return a From 1d06a8d72d35a0793e35d140511e20764782756a Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 26 Jan 2023 09:42:44 -0800 Subject: [PATCH 5/6] update README for playlist browse+play support --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2adb178..b5643c1 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ Slightly outdated screenshots of Supersonic running against the Navidrome Date: Thu, 26 Jan 2023 09:48:44 -0800 Subject: [PATCH 6/6] quick fix --- ui/browsing/playlistpage.go | 1 - ui/browsing/playlistspage.go | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index dbdadc0..87fd7b1 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -128,7 +128,6 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { page.onPlayTrackAt(0) }) - // Todo: there's got to be a way to make this less convoluted. Custom layout? a.container = container.NewVBox(a.titleLabel, container.New(&layouts.VboxCustomPadding{ExtraPad: -10}, a.descriptionLabel, a.ownerLabel, diff --git a/ui/browsing/playlistspage.go b/ui/browsing/playlistspage.go index 05d110b..8c6fd67 100644 --- a/ui/browsing/playlistspage.go +++ b/ui/browsing/playlistspage.go @@ -113,7 +113,7 @@ func NewPlaylistList() *PlaylistList { row := item.(*PlaylistListRow) row.ID = a.Playlists[id].ID row.nameLabel.Text = a.Playlists[id].Name - row.descrptionLabel.Text = a.Playlists[id].Name + row.descrptionLabel.Text = a.Playlists[id].Comment row.ownerLabel.Text = a.Playlists[id].Owner row.trackCountLabel.Text = strconv.Itoa(a.Playlists[id].SongCount) row.Refresh()