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) }