From e7197824371f54b416ef0e08082dee07d8eeb296 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 17 Feb 2024 16:50:50 -0800 Subject: [PATCH 01/28] beginning of work on fullscreen page --- ui/bottompanel.go | 10 +-- ui/browsing/fullscreenpage.go | 78 ++++++++++++++++++++ ui/browsing/router.go | 4 +- ui/controller/routes.go | 5 ++ ui/widgets/largenowplayingcard.go | 115 ++++++++++++++++++++++++++++++ ui/widgets/nowplayingcard.go | 6 +- 6 files changed, 206 insertions(+), 12 deletions(-) create mode 100644 ui/browsing/fullscreenpage.go create mode 100644 ui/widgets/largenowplayingcard.go diff --git a/ui/bottompanel.go b/ui/bottompanel.go index e80f504..a4210f3 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -2,7 +2,6 @@ package ui import ( "image" - "log" "time" "github.com/dweymouth/supersonic/backend" @@ -53,13 +52,8 @@ func NewBottomPanel(pm *backend.PlaybackManager, contr *controller.Controller) * }) bp.NowPlaying = widgets.NewNowPlayingCard() - bp.NowPlaying.OnShowCoverImage = func() { - im, err := bp.ImageManager.GetFullSizeCoverArt(bp.coverArtID) - if err != nil { - log.Printf("error getting full size cover image: %s", err.Error()) - } else { - contr.ShowPopUpImage(im) - } + bp.NowPlaying.OnCoverTapped = func() { + contr.NavigateTo(controller.FullscreenRoute()) } bp.NowPlaying.OnSetFavorite = func(fav bool) { contr.SetTrackFavorites([]string{pm.NowPlaying().ID}, fav) diff --git a/ui/browsing/fullscreenpage.go b/ui/browsing/fullscreenpage.go new file mode 100644 index 0000000..668647e --- /dev/null +++ b/ui/browsing/fullscreenpage.go @@ -0,0 +1,78 @@ +package browsing + +import ( + "github.com/dweymouth/supersonic/backend" + "github.com/dweymouth/supersonic/backend/mediaprovider" + "github.com/dweymouth/supersonic/sharedutil" + "github.com/dweymouth/supersonic/ui/controller" + "github.com/dweymouth/supersonic/ui/util" + "github.com/dweymouth/supersonic/ui/widgets" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" +) + +type FullscreenPage struct { + widget.BaseWidget + + fullscreenPageState + + queue []*mediaprovider.Track + + card *widgets.LargeNowPlayingCard + nowPlayingID string +} + +type fullscreenPageState struct { + contr *controller.Controller + pool *util.WidgetPool + pm *backend.PlaybackManager + canRate bool +} + +func NewFullscreenPage( + contr *controller.Controller, + pool *util.WidgetPool, + pm *backend.PlaybackManager, + canRate bool, +) *FullscreenPage { + a := &FullscreenPage{fullscreenPageState: fullscreenPageState{ + contr: contr, pool: pool, pm: pm, canRate: canRate, + }} + a.ExtendBaseWidget(a) + + a.card = widgets.NewLargeNowPlayingCard() + + a.Reload() + return a +} + +func (a *FullscreenPage) CreateRenderer() fyne.WidgetRenderer { + container := container.NewPadded() + return widget.NewSimpleRenderer(container) +} + +func (a *FullscreenPage) Save() SavedPage { + nps := a.fullscreenPageState + return &nps +} + +func (a *FullscreenPage) Route() controller.Route { + return controller.NowPlayingRoute("") +} + +var _ CanShowNowPlaying = (*FullscreenPage)(nil) + +func (a *FullscreenPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Track) { + a.nowPlayingID = sharedutil.TrackIDOrEmptyStr(song) + a.card.Update(song.Name, song.ArtistNames, song.ArtistIDs, song.Album) +} + +func (a *FullscreenPage) Reload() { + a.queue = a.pm.GetPlayQueue() +} + +func (s *fullscreenPageState) Restore() Page { + return NewFullscreenPage(s.contr, s.pool, s.pm, s.canRate) +} diff --git a/ui/browsing/router.go b/ui/browsing/router.go index 4c5b418..0a2a188 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -30,6 +30,7 @@ func NewRouter(app *backend.App, controller *controller.Controller, nav Navigati } func (r Router) CreatePage(rte controller.Route) Page { + _, canRate := r.App.ServerManager.Server.(mediaprovider.SupportsRating) switch rte.Page { case controller.Album: return NewAlbumPage(rte.Arg, &r.App.Config.AlbumPage, r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager, r.Controller) @@ -41,12 +42,13 @@ func (r Router) CreatePage(rte controller.Route) Page { return NewArtistsPage(r.Controller, r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Favorites: return NewFavoritesPage(&r.App.Config.FavoritesPage, r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager) + case controller.Fullscreen: + return NewFullscreenPage(r.Controller, r.widgetPool, r.App.PlaybackManager, canRate) case controller.Genre: return NewGenrePage(rte.Arg, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Genres: return NewGenresPage(r.Controller, r.App.ServerManager.Server) case controller.NowPlaying: - _, canRate := r.App.ServerManager.Server.(mediaprovider.SupportsRating) return NewNowPlayingPage(rte.Arg, r.Controller, r.widgetPool, &r.App.Config.NowPlayingPage, r.App.PlaybackManager, canRate) case controller.Playlist: return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.widgetPool, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager) diff --git a/ui/controller/routes.go b/ui/controller/routes.go index dc2255a..172175a 100644 --- a/ui/controller/routes.go +++ b/ui/controller/routes.go @@ -8,6 +8,7 @@ const ( Albums Artist Artists + Fullscreen Genre Genres Favorites @@ -38,6 +39,10 @@ func FavoritesRoute() Route { return Route{Page: Favorites} } +func FullscreenRoute() Route { + return Route{Page: Fullscreen} +} + func GenreRoute(genre string) Route { return Route{Page: Genre, Arg: genre} } diff --git a/ui/widgets/largenowplayingcard.go b/ui/widgets/largenowplayingcard.go new file mode 100644 index 0000000..3edf36e --- /dev/null +++ b/ui/widgets/largenowplayingcard.go @@ -0,0 +1,115 @@ +package widgets + +import ( + "image" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" + "github.com/dweymouth/supersonic/ui/theme" +) + +// Shows the current album art, track name, artist name, and album name +// for the currently playing track. Placed into the left side of the BottomPanel. +type LargeNowPlayingCard struct { + widget.BaseWidget + + DisableRating bool + + trackName *widget.Hyperlink + artistName *MultiHyperlink + albumName *widget.Hyperlink + cover *ImagePlaceholder + + OnTrackNameTapped func() + OnArtistNameTapped func(artistID string) + OnAlbumNameTapped func() + OnShowCoverImage func() + OnSetRating func(rating int) + OnSetFavorite func(favorite bool) + OnAddToPlaylist func() +} + +func NewLargeNowPlayingCard() *LargeNowPlayingCard { + n := &LargeNowPlayingCard{ + trackName: widget.NewHyperlink("", nil), + artistName: NewMultiHyperlink(), + albumName: widget.NewHyperlink("", nil), + } + n.ExtendBaseWidget(n) + n.cover = NewImagePlaceholder(theme.TracksIcon, 300) + n.trackName.Hidden = true + n.albumName.Hidden = true + n.albumName.Truncation = fyne.TextTruncateEllipsis + n.trackName.Truncation = fyne.TextTruncateEllipsis + n.trackName.TextStyle.Bold = true + n.albumName.OnTapped = n.onAlbumNameTapped + n.artistName.OnTapped = n.onArtistNameTapped + n.trackName.OnTapped = n.onTrackNameTapped + + return n +} + +func (n *LargeNowPlayingCard) MinSize() fyne.Size { + // prop up height for when cover image is hidden + return fyne.NewSize(n.BaseWidget.MinSize().Width, 85) +} + +func (n *LargeNowPlayingCard) onAlbumNameTapped() { + if n.OnAlbumNameTapped != nil { + n.OnAlbumNameTapped() + } +} + +func (n *LargeNowPlayingCard) onArtistNameTapped(artistID string) { + if n.OnArtistNameTapped != nil { + n.OnArtistNameTapped(artistID) + } +} + +func (n *LargeNowPlayingCard) onTrackNameTapped() { + if n.OnTrackNameTapped != nil { + n.OnTrackNameTapped() + } +} + +func (n *LargeNowPlayingCard) onShowCoverImage(*fyne.PointEvent) { + if n.OnShowCoverImage != nil { + n.OnShowCoverImage() + } +} + +func (n *LargeNowPlayingCard) onSetFavorite(fav bool) { + if n.OnSetFavorite != nil { + n.OnSetFavorite(fav) + } +} + +func (n *LargeNowPlayingCard) onSetRating(rating int) { + if n.OnSetRating != nil { + n.OnSetRating(rating) + } +} + +func (n *LargeNowPlayingCard) CreateRenderer() fyne.WidgetRenderer { + vbox := container.NewVBox( + n.trackName, + n.albumName, + n.artistName, + ) + c := container.NewBorder(nil, vbox, nil, nil, n.cover) + return widget.NewSimpleRenderer(c) +} + +func (n *LargeNowPlayingCard) Update(track string, artists, artistIDs []string, album string) { + n.trackName.SetText(track) + n.trackName.Hidden = track == "" + n.artistName.BuildSegments(artists, artistIDs) + n.albumName.SetText(album) + n.albumName.Hidden = album == "" + n.Refresh() +} + +func (n *LargeNowPlayingCard) SetCoverImage(im image.Image) { + n.cover.SetImage(im, false) +} diff --git a/ui/widgets/nowplayingcard.go b/ui/widgets/nowplayingcard.go index ec3634d..13f882e 100644 --- a/ui/widgets/nowplayingcard.go +++ b/ui/widgets/nowplayingcard.go @@ -30,7 +30,7 @@ type NowPlayingCard struct { OnTrackNameTapped func() OnArtistNameTapped func(artistID string) OnAlbumNameTapped func() - OnShowCoverImage func() + OnCoverTapped func() OnSetRating func(rating int) OnSetFavorite func(favorite bool) OnAddToPlaylist func() @@ -84,8 +84,8 @@ func (n *NowPlayingCard) onTrackNameTapped() { } func (n *NowPlayingCard) onShowCoverImage(*fyne.PointEvent) { - if n.OnShowCoverImage != nil { - n.OnShowCoverImage() + if n.OnCoverTapped != nil { + n.OnCoverTapped() } } From c473c3ad2321d40458f7bb73b7879094d4ea0075 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 23 Feb 2024 09:06:58 -0800 Subject: [PATCH 02/28] some tweaks, still WIP --- ui/browsing/fullscreenpage.go | 10 ++++++++-- ui/browsing/router.go | 2 +- ui/widgets/largenowplayingcard.go | 8 ++------ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/ui/browsing/fullscreenpage.go b/ui/browsing/fullscreenpage.go index 668647e..d30d7c6 100644 --- a/ui/browsing/fullscreenpage.go +++ b/ui/browsing/fullscreenpage.go @@ -10,6 +10,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" ) @@ -28,12 +29,14 @@ type fullscreenPageState struct { contr *controller.Controller pool *util.WidgetPool pm *backend.PlaybackManager + im *backend.ImageManager canRate bool } func NewFullscreenPage( contr *controller.Controller, pool *util.WidgetPool, + im *backend.ImageManager, pm *backend.PlaybackManager, canRate bool, ) *FullscreenPage { @@ -49,7 +52,10 @@ func NewFullscreenPage( } func (a *FullscreenPage) CreateRenderer() fyne.WidgetRenderer { - container := container.NewPadded() + container := container.NewGridWithColumns(2, + a.card, + layout.NewSpacer(), + ) return widget.NewSimpleRenderer(container) } @@ -74,5 +80,5 @@ func (a *FullscreenPage) Reload() { } func (s *fullscreenPageState) Restore() Page { - return NewFullscreenPage(s.contr, s.pool, s.pm, s.canRate) + return NewFullscreenPage(s.contr, s.pool, s.im, s.pm, s.canRate) } diff --git a/ui/browsing/router.go b/ui/browsing/router.go index 0a2a188..f515a16 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -43,7 +43,7 @@ func (r Router) CreatePage(rte controller.Route) Page { case controller.Favorites: return NewFavoritesPage(&r.App.Config.FavoritesPage, r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager) case controller.Fullscreen: - return NewFullscreenPage(r.Controller, r.widgetPool, r.App.PlaybackManager, canRate) + return NewFullscreenPage(r.Controller, r.widgetPool, r.App.ImageManager, r.App.PlaybackManager, canRate) case controller.Genre: return NewGenrePage(rte.Arg, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Genres: diff --git a/ui/widgets/largenowplayingcard.go b/ui/widgets/largenowplayingcard.go index 3edf36e..fda8d89 100644 --- a/ui/widgets/largenowplayingcard.go +++ b/ui/widgets/largenowplayingcard.go @@ -50,11 +50,6 @@ func NewLargeNowPlayingCard() *LargeNowPlayingCard { return n } -func (n *LargeNowPlayingCard) MinSize() fyne.Size { - // prop up height for when cover image is hidden - return fyne.NewSize(n.BaseWidget.MinSize().Width, 85) -} - func (n *LargeNowPlayingCard) onAlbumNameTapped() { if n.OnAlbumNameTapped != nil { n.OnAlbumNameTapped() @@ -97,7 +92,8 @@ func (n *LargeNowPlayingCard) CreateRenderer() fyne.WidgetRenderer { n.albumName, n.artistName, ) - c := container.NewBorder(nil, vbox, nil, nil, n.cover) + c := container.NewCenter( + container.NewBorder(nil, vbox, nil, nil, n.cover)) return widget.NewSimpleRenderer(c) } From 0c8914060e58654e26e5dc939cc33b30e886c973 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 28 Feb 2024 08:32:36 -0800 Subject: [PATCH 03/28] add CaptionedImage widget --- ui/widgets/captionedimage.go | 113 +++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 ui/widgets/captionedimage.go diff --git a/ui/widgets/captionedimage.go b/ui/widgets/captionedimage.go new file mode 100644 index 0000000..0c9f4e3 --- /dev/null +++ b/ui/widgets/captionedimage.go @@ -0,0 +1,113 @@ +package widgets + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/widget" +) + +type Aspectable interface { + fyne.CanvasObject + Aspect() float32 +} + +type CaptionedImage struct { + widget.BaseWidget + + Content Aspectable + Caption fyne.CanvasObject +} + +var _ fyne.Widget = (*CaptionedImage)(nil) + +func NewCaptionedImage(content Aspectable, caption fyne.CanvasObject) *CaptionedImage { + c := &CaptionedImage{Content: content, Caption: caption} + c.ExtendBaseWidget(c) + return c +} + +func (c *CaptionedImage) CreateRenderer() fyne.WidgetRenderer { + return &captionedImageRenderer{ + ci: c, + } +} + +type captionedImageRenderer struct { + ci *CaptionedImage +} + +var _ fyne.WidgetRenderer = (*captionedImageRenderer)(nil) + +func (*captionedImageRenderer) Destroy() { + // intentionally blank +} + +func (c *captionedImageRenderer) Layout(s fyne.Size) { + content := c.ci.Content + aspect := content.Aspect() + caption := c.ci.Caption + var captionHeight float32 + if caption != nil { + captionHeight = caption.MinSize().Height + } + + // max height that could be allocated to Content + contentMaxedHeight := s.Height + if caption != nil { + contentMaxedHeight -= captionHeight + } + + // aspect ratio of Content if it were maxed out + maxedAspect := s.Width / contentMaxedHeight + if maxedAspect > aspect { + // Content will use full height, but not full width + contentW := contentMaxedHeight * aspect + content.Resize(fyne.NewSize(contentW, contentMaxedHeight)) + contentX := (s.Width - contentW) / 2 + content.Move(fyne.NewPos(contentX, 0)) + if caption != nil { + caption.Resize(fyne.NewSize(s.Width, captionHeight)) + caption.Move(fyne.NewPos(0, contentMaxedHeight)) + } + return + } + + // Content will not use full height + // Positioning of Content and Caption will be adjusted to center + contentH := s.Width / aspect + yStart := (s.Height - contentH - captionHeight) / 2 + content.Resize(fyne.NewSize(s.Width, contentH)) + content.Move(fyne.NewPos(0, yStart)) + if caption != nil { + caption.Resize(fyne.NewSize(s.Width, captionHeight)) + caption.Move(fyne.NewPos(0, yStart+contentH)) + } +} + +func (c *captionedImageRenderer) MinSize() fyne.Size { + objSize := c.ci.Content.MinSize() + if c.ci.Caption == nil { + return objSize + } + cptSize := c.ci.Caption.MinSize() + return fyne.NewSize( + fyne.Max(objSize.Width, cptSize.Width), + objSize.Height+cptSize.Height, + ) +} + +func (c *captionedImageRenderer) Objects() []fyne.CanvasObject { + if c.ci.Caption != nil { + return []fyne.CanvasObject{c.ci.Content, c.ci.Caption} + } + return []fyne.CanvasObject{c.ci.Content} +} + +func (c *captionedImageRenderer) Refresh() { + c.ci.Content.Refresh() + if cap := c.ci.Caption; cap != nil { + cap.Refresh() + } + // aspect may have changed on Content, + // so we need to re-layout + c.Layout(c.ci.Size()) +} From 4023d9e76e0afa77c1e4a29586f446b873084a0e Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 28 Feb 2024 19:44:07 -0800 Subject: [PATCH 04/28] add padding around large now playing card --- ui/browsing/fullscreenpage.go | 6 ++++- ui/layouts/percentpadlayout.go | 44 +++++++++++++++++++++++++++++++ ui/widgets/captionedimage.go | 14 +++++----- ui/widgets/imageplaceholder.go | 7 +++++ ui/widgets/largenowplayingcard.go | 24 ++++++++--------- 5 files changed, 74 insertions(+), 21 deletions(-) create mode 100644 ui/layouts/percentpadlayout.go diff --git a/ui/browsing/fullscreenpage.go b/ui/browsing/fullscreenpage.go index d30d7c6..bb28d01 100644 --- a/ui/browsing/fullscreenpage.go +++ b/ui/browsing/fullscreenpage.go @@ -5,6 +5,7 @@ import ( "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/ui/controller" + "github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/util" "github.com/dweymouth/supersonic/ui/widgets" @@ -53,7 +54,10 @@ func NewFullscreenPage( func (a *FullscreenPage) CreateRenderer() fyne.WidgetRenderer { container := container.NewGridWithColumns(2, - a.card, + container.New(&layouts.PercentPadLayout{ + LeftRightObjectPercent: .8, + TopBottomObjectPercent: .8, + }, a.card), layout.NewSpacer(), ) return widget.NewSimpleRenderer(container) diff --git a/ui/layouts/percentpadlayout.go b/ui/layouts/percentpadlayout.go new file mode 100644 index 0000000..1ec8411 --- /dev/null +++ b/ui/layouts/percentpadlayout.go @@ -0,0 +1,44 @@ +package layouts + +import "fyne.io/fyne/v2" + +// Centers and pads a given item by a percent (0-1) +// of the width and height that should be taken by the objects +type PercentPadLayout struct { + LeftRightObjectPercent float32 + TopBottomObjectPercent float32 +} + +func (l *PercentPadLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + var objMinSize fyne.Size + for _, obj := range objects { + objMinSize = objMinSize.Max(obj.MinSize()) + } + if objMinSize.IsZero() { + return objMinSize + } + + return fyne.Size{ + Width: objMinSize.Width / l.LeftRightObjectPercent, + Height: objMinSize.Height / l.LeftRightObjectPercent, + } +} + +func (l *PercentPadLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + if len(objects) == 0 { + return + } + + objSize := fyne.NewSize( + size.Width*l.LeftRightObjectPercent, + size.Height*l.TopBottomObjectPercent, + ) + objPos := fyne.NewPos( + size.Width*(1-l.LeftRightObjectPercent)/2, + size.Height*(1-l.TopBottomObjectPercent)/2, + ) + for _, obj := range objects { + obj.Resize(objSize) + obj.Move(objPos) + } +} diff --git a/ui/widgets/captionedimage.go b/ui/widgets/captionedimage.go index 0c9f4e3..83d858d 100644 --- a/ui/widgets/captionedimage.go +++ b/ui/widgets/captionedimage.go @@ -59,14 +59,14 @@ func (c *captionedImageRenderer) Layout(s fyne.Size) { // aspect ratio of Content if it were maxed out maxedAspect := s.Width / contentMaxedHeight if maxedAspect > aspect { - // Content will use full height, but not full width - contentW := contentMaxedHeight * aspect - content.Resize(fyne.NewSize(contentW, contentMaxedHeight)) - contentX := (s.Width - contentW) / 2 - content.Move(fyne.NewPos(contentX, 0)) + // Will use full height, but not full width + width := contentMaxedHeight * aspect + content.Resize(fyne.NewSize(width, contentMaxedHeight)) + xStart := (s.Width - width) / 2 + content.Move(fyne.NewPos(xStart, 0)) if caption != nil { - caption.Resize(fyne.NewSize(s.Width, captionHeight)) - caption.Move(fyne.NewPos(0, contentMaxedHeight)) + caption.Resize(fyne.NewSize(width, captionHeight)) + caption.Move(fyne.NewPos(xStart, contentMaxedHeight)) } return } diff --git a/ui/widgets/imageplaceholder.go b/ui/widgets/imageplaceholder.go index 03c3a82..de552ae 100644 --- a/ui/widgets/imageplaceholder.go +++ b/ui/widgets/imageplaceholder.go @@ -51,6 +51,13 @@ func NewImagePlaceholder(centerIcon fyne.Resource, minSize float32) *ImagePlaceh return i } +func (i *ImagePlaceholder) Aspect() float32 { + if i.image != nil { + return i.imageDisp.Aspect() + } + return 1.0 +} + func (i *ImagePlaceholder) HaveImage() bool { return i.image != nil } diff --git a/ui/widgets/largenowplayingcard.go b/ui/widgets/largenowplayingcard.go index fda8d89..768f2d6 100644 --- a/ui/widgets/largenowplayingcard.go +++ b/ui/widgets/largenowplayingcard.go @@ -6,13 +6,14 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" + "github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/theme" ) // Shows the current album art, track name, artist name, and album name // for the currently playing track. Placed into the left side of the BottomPanel. type LargeNowPlayingCard struct { - widget.BaseWidget + CaptionedImage DisableRating bool @@ -35,9 +36,17 @@ func NewLargeNowPlayingCard() *LargeNowPlayingCard { trackName: widget.NewHyperlink("", nil), artistName: NewMultiHyperlink(), albumName: widget.NewHyperlink("", nil), + cover: NewImagePlaceholder(theme.TracksIcon, 300), } n.ExtendBaseWidget(n) - n.cover = NewImagePlaceholder(theme.TracksIcon, 300) + // set up the layout + n.Content = n.cover + n.Caption = container.New(&layouts.VboxCustomPadding{ExtraPad: -13}, + n.trackName, + n.albumName, + n.artistName, + ) + n.trackName.Hidden = true n.albumName.Hidden = true n.albumName.Truncation = fyne.TextTruncateEllipsis @@ -86,17 +95,6 @@ func (n *LargeNowPlayingCard) onSetRating(rating int) { } } -func (n *LargeNowPlayingCard) CreateRenderer() fyne.WidgetRenderer { - vbox := container.NewVBox( - n.trackName, - n.albumName, - n.artistName, - ) - c := container.NewCenter( - container.NewBorder(nil, vbox, nil, nil, n.cover)) - return widget.NewSimpleRenderer(c) -} - func (n *LargeNowPlayingCard) Update(track string, artists, artistIDs []string, album string) { n.trackName.SetText(track) n.trackName.Hidden = track == "" From 9ed2d8502b851f5584a1918e20bebd1c6f9a8096 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 1 Mar 2024 08:45:36 -0800 Subject: [PATCH 05/28] add GetFullSizeCoverArtAsync --- backend/imagemanager.go | 59 ++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/backend/imagemanager.go b/backend/imagemanager.go index 96da5bf..f8102ee 100644 --- a/backend/imagemanager.go +++ b/backend/imagemanager.go @@ -136,20 +136,26 @@ func (i *ImageManager) GetFullSizeCoverArt(coverID string) (image.Image, error) i.cachedFullSizeCoverAccessedAt = time.Now().UnixMilli() return i.cachedFullSizeCover, nil } - if i.s.Server == nil { - return nil, errors.New("logged out") - } + return i.getFullSizeCoverArtFromServer(context.Background(), coverID, nil) +} - i.serverFetchSema <- struct{}{} // acquire - im, err := i.s.Server.GetCoverArt(coverID, 0) - <-i.serverFetchSema // release - if err != nil { - return nil, err - } - i.cachedFullSizeCover = im - i.cachedFullSizeCoverID = coverID - i.cachedFullSizeCoverAccessedAt = time.Now().UnixMilli() - return im, nil +// GetCoverThumbnailAsync asynchronously fetches the cover image for the given ID, +// and invokes the callback on completion. It returns a context.CancelFunc which can be used to +// cancel the fetch. The callback will not be invoked if the fetch is cancelled before completion. +// The cancel func must be invoked to avoid resource leaks. Use GetCoverThumbnail if cancellation is not needed. +func (i *ImageManager) GetFullSizeCoverArtAsync(coverID string, cb func(image.Image, error)) context.CancelFunc { + ctx, cancel := context.WithCancel(context.Background()) + go func() { + if i.cachedFullSizeCoverID == coverID { + i.cachedFullSizeCoverAccessedAt = time.Now().UnixMilli() + if ctx.Err() == nil { + cb(i.cachedFullSizeCover, nil) + } + } else { + i.getFullSizeCoverArtFromServer(ctx, coverID, cb) + } + }() + return cancel } // GetCoverArtURL returns the URL for the locally cached cover thumbnail, if it exists. @@ -267,6 +273,33 @@ func (i *ImageManager) fetchAndCacheCoverFromServer(ctx context.Context, coverID } } +func (i *ImageManager) getFullSizeCoverArtFromServer(ctx context.Context, coverID string, cb func(image.Image, error)) (image.Image, error) { + if i.s.Server == nil { + err := errors.New("logged out") + if ctx.Err() == nil && cb != nil { + cb(nil, err) + } + return nil, err + } + + select { + case <-ctx.Done(): + return nil, context.Canceled + case i.serverFetchSema <- struct{}{}: // acquire + im, err := i.s.Server.GetCoverArt(coverID, 0) + <-i.serverFetchSema // release + if err == nil { + i.cachedFullSizeCover = im + i.cachedFullSizeCoverID = coverID + i.cachedFullSizeCoverAccessedAt = time.Now().UnixMilli() + } + if ctx.Err() == nil && cb != nil { + cb(im, err) + } + return im, err + } +} + func (i *ImageManager) checkRefreshLocalCover(stat os.FileInfo, coverID string, ttl time.Duration) { if time.Since(stat.ModTime()) > cachedImageValidTime { i.fetchAndCacheCoverFromServer(context.Background(), coverID, ttl, nil) From f1e5c554615fdded87bffc54290bce7dde2cb5f7 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 1 Mar 2024 09:21:10 -0800 Subject: [PATCH 06/28] more work on fullscreen page --- ui/browsing/fullscreenpage.go | 47 ++++++++++++++++++++++++------- ui/widgets/largenowplayingcard.go | 35 ++++++++++++++--------- 2 files changed, 58 insertions(+), 24 deletions(-) diff --git a/ui/browsing/fullscreenpage.go b/ui/browsing/fullscreenpage.go index bb28d01..cd6fab1 100644 --- a/ui/browsing/fullscreenpage.go +++ b/ui/browsing/fullscreenpage.go @@ -1,6 +1,10 @@ package browsing import ( + "context" + "image" + "log" + "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/sharedutil" @@ -20,10 +24,11 @@ type FullscreenPage struct { fullscreenPageState - queue []*mediaprovider.Track - - card *widgets.LargeNowPlayingCard - nowPlayingID string + queue []*mediaprovider.Track + imageLoadCancel context.CancelFunc + card *widgets.LargeNowPlayingCard + nowPlayingID string + albumID string } type fullscreenPageState struct { @@ -42,28 +47,39 @@ func NewFullscreenPage( canRate bool, ) *FullscreenPage { a := &FullscreenPage{fullscreenPageState: fullscreenPageState{ - contr: contr, pool: pool, pm: pm, canRate: canRate, + contr: contr, pool: pool, im: im, pm: pm, canRate: canRate, }} a.ExtendBaseWidget(a) a.card = widgets.NewLargeNowPlayingCard() + a.card.OnAlbumNameTapped = func() { contr.NavigateTo(controller.AlbumRoute(a.albumID)) } + a.card.OnArtistNameTapped = func(artistID string) { contr.NavigateTo(controller.ArtistRoute(artistID)) } a.Reload() return a } func (a *FullscreenPage) CreateRenderer() fyne.WidgetRenderer { + paddedLayout := &layouts.PercentPadLayout{ + LeftRightObjectPercent: .8, + TopBottomObjectPercent: .8, + } container := container.NewGridWithColumns(2, - container.New(&layouts.PercentPadLayout{ - LeftRightObjectPercent: .8, - TopBottomObjectPercent: .8, - }, a.card), - layout.NewSpacer(), + container.New(paddedLayout, a.card), + container.New(paddedLayout, + util.AddHeaderBackground( + container.NewAppTabs( + container.NewTabItem("Up Next", layout.NewSpacer()), + container.NewTabItem("Lyrics", layout.NewSpacer()), + ))), ) return widget.NewSimpleRenderer(container) } func (a *FullscreenPage) Save() SavedPage { + if a.imageLoadCancel != nil { + a.imageLoadCancel() + } nps := a.fullscreenPageState return &nps } @@ -75,8 +91,19 @@ func (a *FullscreenPage) Route() controller.Route { var _ CanShowNowPlaying = (*FullscreenPage)(nil) func (a *FullscreenPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Track) { + if a.imageLoadCancel != nil { + a.imageLoadCancel() + } + a.albumID = song.AlbumID a.nowPlayingID = sharedutil.TrackIDOrEmptyStr(song) a.card.Update(song.Name, song.ArtistNames, song.ArtistIDs, song.Album) + a.imageLoadCancel = a.im.GetFullSizeCoverArtAsync(song.CoverArtID, func(img image.Image, err error) { + if err != nil { + log.Printf("error loading cover art: %v\n", err) + } else { + a.card.SetCoverImage(img) + } + }) } func (a *FullscreenPage) Reload() { diff --git a/ui/widgets/largenowplayingcard.go b/ui/widgets/largenowplayingcard.go index 768f2d6..b1a1bcb 100644 --- a/ui/widgets/largenowplayingcard.go +++ b/ui/widgets/largenowplayingcard.go @@ -5,9 +5,11 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "github.com/dweymouth/supersonic/ui/layouts" - "github.com/dweymouth/supersonic/ui/theme" + myTheme "github.com/dweymouth/supersonic/ui/theme" ) // Shows the current album art, track name, artist name, and album name @@ -17,12 +19,13 @@ type LargeNowPlayingCard struct { DisableRating bool - trackName *widget.Hyperlink + trackName *widget.RichText artistName *MultiHyperlink albumName *widget.Hyperlink + rating *StarRating + favorite *widget.Icon cover *ImagePlaceholder - OnTrackNameTapped func() OnArtistNameTapped func(artistID string) OnAlbumNameTapped func() OnShowCoverImage func() @@ -33,28 +36,38 @@ type LargeNowPlayingCard struct { func NewLargeNowPlayingCard() *LargeNowPlayingCard { n := &LargeNowPlayingCard{ - trackName: widget.NewHyperlink("", nil), + trackName: widget.NewRichTextWithText(""), artistName: NewMultiHyperlink(), albumName: widget.NewHyperlink("", nil), - cover: NewImagePlaceholder(theme.TracksIcon, 300), + rating: NewStarRating(), + favorite: widget.NewIcon(myTheme.NotFavoriteIcon), + cover: NewImagePlaceholder(myTheme.TracksIcon, 300), } n.ExtendBaseWidget(n) + n.rating.StarSize = theme.IconInlineSize() + theme.InnerPadding()/2 // set up the layout n.Content = n.cover n.Caption = container.New(&layouts.VboxCustomPadding{ExtraPad: -13}, n.trackName, n.albumName, n.artistName, + container.NewHBox( + layout.NewSpacer(), + n.favorite, + widget.NewLabel("·"), + n.rating, + layout.NewSpacer(), + ), ) + n.trackName.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameSubHeadingText + n.trackName.Truncation = fyne.TextTruncateEllipsis n.trackName.Hidden = true n.albumName.Hidden = true n.albumName.Truncation = fyne.TextTruncateEllipsis n.trackName.Truncation = fyne.TextTruncateEllipsis - n.trackName.TextStyle.Bold = true n.albumName.OnTapped = n.onAlbumNameTapped n.artistName.OnTapped = n.onArtistNameTapped - n.trackName.OnTapped = n.onTrackNameTapped return n } @@ -71,12 +84,6 @@ func (n *LargeNowPlayingCard) onArtistNameTapped(artistID string) { } } -func (n *LargeNowPlayingCard) onTrackNameTapped() { - if n.OnTrackNameTapped != nil { - n.OnTrackNameTapped() - } -} - func (n *LargeNowPlayingCard) onShowCoverImage(*fyne.PointEvent) { if n.OnShowCoverImage != nil { n.OnShowCoverImage() @@ -96,7 +103,7 @@ func (n *LargeNowPlayingCard) onSetRating(rating int) { } func (n *LargeNowPlayingCard) Update(track string, artists, artistIDs []string, album string) { - n.trackName.SetText(track) + n.trackName.Segments[0].(*widget.TextSegment).Text = track n.trackName.Hidden = track == "" n.artistName.BuildSegments(artists, artistIDs) n.albumName.SetText(album) From fee1dd66bb0c2fbcf9ab130362a1e745ad7e6d93 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 1 Mar 2024 18:12:25 -0800 Subject: [PATCH 07/28] add FavoriteIcon and use in fullscreen page --- ui/browsing/fullscreenpage.go | 2 +- ui/widgets/favoriteicon.go | 27 +++++++++++++++++++++++++++ ui/widgets/largenowplayingcard.go | 28 ++++++++++++++++++---------- ui/widgets/tracklist.go | 27 ++++++++------------------- 4 files changed, 54 insertions(+), 30 deletions(-) create mode 100644 ui/widgets/favoriteicon.go diff --git a/ui/browsing/fullscreenpage.go b/ui/browsing/fullscreenpage.go index cd6fab1..f6d0196 100644 --- a/ui/browsing/fullscreenpage.go +++ b/ui/browsing/fullscreenpage.go @@ -96,7 +96,7 @@ func (a *FullscreenPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Tr } a.albumID = song.AlbumID a.nowPlayingID = sharedutil.TrackIDOrEmptyStr(song) - a.card.Update(song.Name, song.ArtistNames, song.ArtistIDs, song.Album) + a.card.Update(song) a.imageLoadCancel = a.im.GetFullSizeCoverArtAsync(song.CoverArtID, func(img image.Image, err error) { if err != nil { log.Printf("error loading cover art: %v\n", err) diff --git a/ui/widgets/favoriteicon.go b/ui/widgets/favoriteicon.go new file mode 100644 index 0000000..5f1f2ce --- /dev/null +++ b/ui/widgets/favoriteicon.go @@ -0,0 +1,27 @@ +package widgets + +import ( + "github.com/dweymouth/supersonic/ui/theme" +) + +type FavoriteIcon struct { + TappableIcon + + Favorite bool +} + +func NewFavoriteIcon() *FavoriteIcon { + f := &FavoriteIcon{} + f.Resource = theme.NotFavoriteIcon + f.ExtendBaseWidget(f) + return f +} + +func (f *FavoriteIcon) Refresh() { + if f.Favorite { + f.Resource = theme.FavoriteIcon + } else { + f.Resource = theme.NotFavoriteIcon + } + f.BaseWidget.Refresh() +} diff --git a/ui/widgets/largenowplayingcard.go b/ui/widgets/largenowplayingcard.go index b1a1bcb..f717b8b 100644 --- a/ui/widgets/largenowplayingcard.go +++ b/ui/widgets/largenowplayingcard.go @@ -8,6 +8,7 @@ import ( "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" + "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/ui/layouts" myTheme "github.com/dweymouth/supersonic/ui/theme" ) @@ -23,7 +24,7 @@ type LargeNowPlayingCard struct { artistName *MultiHyperlink albumName *widget.Hyperlink rating *StarRating - favorite *widget.Icon + favorite *FavoriteIcon cover *ImagePlaceholder OnArtistNameTapped func(artistID string) @@ -40,11 +41,13 @@ func NewLargeNowPlayingCard() *LargeNowPlayingCard { artistName: NewMultiHyperlink(), albumName: widget.NewHyperlink("", nil), rating: NewStarRating(), - favorite: widget.NewIcon(myTheme.NotFavoriteIcon), + favorite: NewFavoriteIcon(), cover: NewImagePlaceholder(myTheme.TracksIcon, 300), } n.ExtendBaseWidget(n) n.rating.StarSize = theme.IconInlineSize() + theme.InnerPadding()/2 + n.rating.OnRatingChanged = n.onSetRating + n.favorite.OnTapped = n.onToggleFavorite // set up the layout n.Content = n.cover n.Caption = container.New(&layouts.VboxCustomPadding{ExtraPad: -13}, @@ -90,9 +93,11 @@ func (n *LargeNowPlayingCard) onShowCoverImage(*fyne.PointEvent) { } } -func (n *LargeNowPlayingCard) onSetFavorite(fav bool) { +func (n *LargeNowPlayingCard) onToggleFavorite() { + n.favorite.Favorite = !n.favorite.Favorite + n.favorite.Refresh() if n.OnSetFavorite != nil { - n.OnSetFavorite(fav) + n.OnSetFavorite(n.favorite.Favorite) } } @@ -102,12 +107,15 @@ func (n *LargeNowPlayingCard) onSetRating(rating int) { } } -func (n *LargeNowPlayingCard) Update(track string, artists, artistIDs []string, album string) { - n.trackName.Segments[0].(*widget.TextSegment).Text = track - n.trackName.Hidden = track == "" - n.artistName.BuildSegments(artists, artistIDs) - n.albumName.SetText(album) - n.albumName.Hidden = album == "" +func (n *LargeNowPlayingCard) Update(track *mediaprovider.Track) { + n.trackName.Segments[0].(*widget.TextSegment).Text = track.Name + n.trackName.Hidden = track.Name == "" + n.artistName.BuildSegments(track.ArtistNames, track.ArtistIDs) + n.albumName.Text = track.Album + n.albumName.Hidden = track.Album == "" + n.rating.Rating = track.Rating + n.favorite.Favorite = track.Favorite + n.Refresh() } diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index f975208..fa873ef 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -761,7 +761,7 @@ func NewTrackRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *TrackRow t.album.OnTapped = func() { tracklist.onAlbumTapped(t.albumID) } t.dur = newTrailingAlignLabel() t.year = newTrailingAlignLabel() - favorite := NewTappableIcon(myTheme.NotFavoriteIcon) + favorite := NewFavoriteIcon() favorite.OnTapped = t.toggleFavorited t.favorite = container.NewCenter(favorite) t.rating = NewStarRating() @@ -868,13 +868,8 @@ func (t *TrackRow) Update(tm *trackModel, rowNum int) { // Update favorite column if tr.Favorite != t.isFavorite { - if tr.Favorite { - t.isFavorite = true - t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.FavoriteIcon - } else { - t.isFavorite = false - t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.NotFavoriteIcon - } + t.isFavorite = tr.Favorite + t.favorite.Objects[0].(*FavoriteIcon).Favorite = tr.Favorite changed = true } @@ -914,17 +909,11 @@ func (t *TrackRow) Update(tm *trackModel, rowNum int) { } func (t *TrackRow) toggleFavorited() { - if t.isFavorite { - t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.NotFavoriteIcon - t.favorite.Refresh() - t.isFavorite = false - t.tracklist.onSetFavorite(t.trackID, false) - } else { - t.favorite.Objects[0].(*TappableIcon).Resource = myTheme.FavoriteIcon - t.favorite.Refresh() - t.isFavorite = true - t.tracklist.onSetFavorite(t.trackID, true) - } + t.isFavorite = !t.isFavorite + favIcon := t.favorite.Objects[0].(*FavoriteIcon) + favIcon.Favorite = t.isFavorite + t.favorite.Refresh() + t.tracklist.onSetFavorite(t.trackID, t.isFavorite) } func (t *TrackRow) setTrackRating(rating int) { From cbbebf6eb5340db5aac44499b23f38ea25aa53cf Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 2 Mar 2024 09:28:35 -0800 Subject: [PATCH 08/28] hook up rating and favorite actions --- ui/browsing/fullscreenpage.go | 15 +++++++++++++-- ui/widgets/largenowplayingcard.go | 8 -------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/ui/browsing/fullscreenpage.go b/ui/browsing/fullscreenpage.go index f6d0196..e992eb6 100644 --- a/ui/browsing/fullscreenpage.go +++ b/ui/browsing/fullscreenpage.go @@ -52,8 +52,19 @@ func NewFullscreenPage( a.ExtendBaseWidget(a) a.card = widgets.NewLargeNowPlayingCard() - a.card.OnAlbumNameTapped = func() { contr.NavigateTo(controller.AlbumRoute(a.albumID)) } - a.card.OnArtistNameTapped = func(artistID string) { contr.NavigateTo(controller.ArtistRoute(artistID)) } + a.card.DisableRating = !canRate + a.card.OnAlbumNameTapped = func() { + contr.NavigateTo(controller.AlbumRoute(a.albumID)) + } + a.card.OnArtistNameTapped = func(artistID string) { + contr.NavigateTo(controller.ArtistRoute(artistID)) + } + a.card.OnSetFavorite = func(fav bool) { + a.contr.SetTrackFavorites([]string{a.nowPlayingID}, fav) + } + a.card.OnSetRating = func(rating int) { + a.contr.SetTrackRatings([]string{a.nowPlayingID}, rating) + } a.Reload() return a diff --git a/ui/widgets/largenowplayingcard.go b/ui/widgets/largenowplayingcard.go index f717b8b..abfb9e9 100644 --- a/ui/widgets/largenowplayingcard.go +++ b/ui/widgets/largenowplayingcard.go @@ -29,10 +29,8 @@ type LargeNowPlayingCard struct { OnArtistNameTapped func(artistID string) OnAlbumNameTapped func() - OnShowCoverImage func() OnSetRating func(rating int) OnSetFavorite func(favorite bool) - OnAddToPlaylist func() } func NewLargeNowPlayingCard() *LargeNowPlayingCard { @@ -87,12 +85,6 @@ func (n *LargeNowPlayingCard) onArtistNameTapped(artistID string) { } } -func (n *LargeNowPlayingCard) onShowCoverImage(*fyne.PointEvent) { - if n.OnShowCoverImage != nil { - n.OnShowCoverImage() - } -} - func (n *LargeNowPlayingCard) onToggleFavorite() { n.favorite.Favorite = !n.favorite.Favorite n.favorite.Refresh() From 18d05804242ee77f3f8ba508b14b008a7dc9ac4d Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 2 Mar 2024 10:04:47 -0800 Subject: [PATCH 09/28] extract some sharable utils out of tracklist in prep for playqueuelist --- ui/util/tracklistutil.go | 64 ++++++++++++++ ui/util/util.go | 18 ++++ ui/widgets/tracklist.go | 177 +++++++++++++-------------------------- 3 files changed, 139 insertions(+), 120 deletions(-) create mode 100644 ui/util/tracklistutil.go diff --git a/ui/util/tracklistutil.go b/ui/util/tracklistutil.go new file mode 100644 index 0000000..00f7de8 --- /dev/null +++ b/ui/util/tracklistutil.go @@ -0,0 +1,64 @@ +package util + +import "github.com/dweymouth/supersonic/backend/mediaprovider" + +type TrackListModel struct { + Track *mediaprovider.Track + Selected bool +} + +func SelectTrack(tracks []*TrackListModel, idx int) { + if tracks[idx].Selected { + return + } + UnselectAllTracks(tracks) + tracks[idx].Selected = true +} + +func SelectAllTracks(tracks []*TrackListModel) { + for _, tm := range tracks { + tm.Selected = true + } +} + +func UnselectAllTracks(tracks []*TrackListModel) { + for _, tm := range tracks { + tm.Selected = false + } +} + +func SelectTrackRange(tracks []*TrackListModel, idx int) { + if tracks[idx].Selected { + return + } + lastSelected := -1 + for i := len(tracks) - 1; i >= 0; i-- { + if tracks[i].Selected { + lastSelected = i + break + } + } + if lastSelected < 0 { + tracks[idx].Selected = true + return + } + from := minInt(idx, lastSelected) + to := maxInt(idx, lastSelected) + for i := from; i <= to; i++ { + tracks[i].Selected = true + } +} + +func minInt(a, b int) int { + if a < b { + return a + } + return b +} + +func maxInt(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/ui/util/util.go b/ui/util/util.go index f00c4ef..8513276 100644 --- a/ui/util/util.go +++ b/ui/util/util.go @@ -212,6 +212,24 @@ func AddHeaderBackground(obj fyne.CanvasObject) *fyne.Container { obj)) } +func NewTruncatingRichText() *widget.RichText { + rt := widget.NewRichTextWithText("") + rt.Truncation = fyne.TextTruncateEllipsis + return rt +} + +func NewTruncatingLabel() *widget.Label { + rt := widget.NewLabel("") + rt.Truncation = fyne.TextTruncateEllipsis + return rt +} + +func NewTrailingAlignLabel() *widget.Label { + rt := widget.NewLabel("") + rt.Alignment = fyne.TextAlignTrailing + return rt +} + type HSpace struct { widget.BaseWidget diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index fa873ef..5d77102 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -99,8 +99,8 @@ type Tracklist struct { sorting TracklistSort tracksMutex sync.RWMutex - tracks []*trackModel - tracksOrigOrder []*trackModel + tracks []*util.TrackListModel + tracksOrigOrder []*util.TrackListModel nowPlayingID string colLayout *layouts.ColumnsLayout @@ -111,11 +111,6 @@ type Tracklist struct { container *fyne.Container } -type trackModel struct { - track *mediaprovider.Track - selected bool -} - func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { t := &Tracklist{visibleColumns: make([]bool, numColumns)} t.ExtendBaseWidget(t) @@ -169,7 +164,7 @@ func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { tr := item.(*TrackRow) t.list.SetItemForID(itemID, tr) - if tr.trackID != model.track.ID || tr.ListItemID != itemID { + if tr.trackID != model.Track.ID || tr.ListItemID != itemID { tr.ListItemID = itemID } i := -1 // signal that we want to display the actual track num. @@ -222,7 +217,7 @@ func (t *Tracklist) TrackAt(idx int) *mediaprovider.Track { log.Println("error: Tracklist.TrackAt: index out of range") return nil } - return t.tracks[idx].track + return t.tracks[idx].Track } func (t *Tracklist) SetVisibleColumns(cols []string) { @@ -336,8 +331,8 @@ func (t *Tracklist) _setTracks(trs []*mediaprovider.Track) { func (t *Tracklist) GetTracks() []*mediaprovider.Track { t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() - return sharedutil.MapSlice(t.tracks, func(tm *trackModel) *mediaprovider.Track { - return tm.track + return sharedutil.MapSlice(t.tracks, func(tm *util.TrackListModel) *mediaprovider.Track { + return tm.Track }) } @@ -351,9 +346,7 @@ func (t *Tracklist) AppendTracks(trs []*mediaprovider.Track) { func (t *Tracklist) SelectAll() { t.tracksMutex.RLock() - for _, tm := range t.tracks { - tm.selected = true - } + util.SelectAllTracks(t.tracks) t.tracksMutex.RUnlock() t.list.Refresh() } @@ -365,9 +358,7 @@ func (t *Tracklist) UnselectAll() { func (t *Tracklist) unselectAll() { t.tracksMutex.RLock() - for _, tm := range t.tracks { - tm.selected = false - } + util.UnselectAllTracks(t.tracks) t.tracksMutex.RUnlock() } @@ -375,11 +366,11 @@ func (t *Tracklist) SelectAndScrollToTrack(trackID string) { t.tracksMutex.RLock() idx := -1 for i, tr := range t.tracks { - if tr.track.ID == trackID { + if tr.Track.ID == trackID { idx = i - tr.selected = true + tr.Selected = true } else { - tr.selected = false + tr.Selected = false } } t.tracksMutex.RUnlock() @@ -397,9 +388,9 @@ func (t *Tracklist) Refresh() { t.BaseWidget.Refresh() } -func toTrackModels(trs []*mediaprovider.Track) []*trackModel { - return sharedutil.MapSlice(trs, func(tr *mediaprovider.Track) *trackModel { - return &trackModel{track: tr, selected: false} +func toTrackModels(trs []*mediaprovider.Track) []*util.TrackListModel { + return sharedutil.MapSlice(trs, func(tr *mediaprovider.Track) *util.TrackListModel { + return &util.TrackListModel{Track: tr, Selected: false} }) } @@ -407,8 +398,8 @@ func toTrackModels(trs []*mediaprovider.Track) []*trackModel { // doesn't fall through to the page (which calls UnselectAll on tracklist) func (t *Tracklist) Tapped(*fyne.PointEvent) {} -func (t *Tracklist) stringSort(fieldFn func(*trackModel) string) { - new := make([]*trackModel, len(t.tracksOrigOrder)) +func (t *Tracklist) stringSort(fieldFn func(*util.TrackListModel) string) { + new := make([]*util.TrackListModel, len(t.tracksOrigOrder)) copy(new, t.tracksOrigOrder) sort.SliceStable(new, func(i, j int) bool { cmp := strings.Compare(fieldFn(new[i]), fieldFn(new[j])) @@ -420,8 +411,8 @@ func (t *Tracklist) stringSort(fieldFn func(*trackModel) string) { t.tracks = new } -func (t *Tracklist) intSort(fieldFn func(*trackModel) int64) { - new := make([]*trackModel, len(t.tracksOrigOrder)) +func (t *Tracklist) intSort(fieldFn func(*util.TrackListModel) int64) { + new := make([]*util.TrackListModel, len(t.tracksOrigOrder)) copy(new, t.tracksOrigOrder) sort.SliceStable(new, func(i, j int) bool { if t.sorting.SortOrder == SortDescending { @@ -445,30 +436,30 @@ func (t *Tracklist) doSortTracks() { t.tracks = t.tracksOrigOrder } case ColumnTitle: - t.stringSort(func(tr *trackModel) string { return tr.track.Name }) + t.stringSort(func(tr *util.TrackListModel) string { return tr.Track.Name }) case ColumnArtist: - t.stringSort(func(tr *trackModel) string { return strings.Join(tr.track.ArtistNames, ", ") }) + t.stringSort(func(tr *util.TrackListModel) string { return strings.Join(tr.Track.ArtistNames, ", ") }) case ColumnAlbum: - t.stringSort(func(tr *trackModel) string { return tr.track.Album }) + t.stringSort(func(tr *util.TrackListModel) string { return tr.Track.Album }) case ColumnPath: - t.stringSort(func(tr *trackModel) string { return tr.track.FilePath }) + t.stringSort(func(tr *util.TrackListModel) string { return tr.Track.FilePath }) case ColumnRating: - t.intSort(func(tr *trackModel) int64 { return int64(tr.track.Rating) }) + t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track.Rating) }) case ColumnTime: - t.intSort(func(tr *trackModel) int64 { return int64(tr.track.Duration) }) + t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track.Duration) }) case ColumnYear: - t.intSort(func(tr *trackModel) int64 { return int64(tr.track.Year) }) + t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track.Year) }) case ColumnSize: - t.intSort(func(tr *trackModel) int64 { return tr.track.Size }) + t.intSort(func(tr *util.TrackListModel) int64 { return tr.Track.Size }) case ColumnPlays: - t.intSort(func(tr *trackModel) int64 { return int64(tr.track.PlayCount) }) + t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track.PlayCount) }) case ColumnComment: - t.stringSort(func(tr *trackModel) string { return tr.track.Comment }) + t.stringSort(func(tr *util.TrackListModel) string { return tr.Track.Comment }) case ColumnBitrate: - t.intSort(func(tr *trackModel) int64 { return int64(tr.track.BitRate) }) + t.intSort(func(tr *util.TrackListModel) int64 { return int64(tr.Track.BitRate) }) case ColumnFavorite: - t.intSort(func(tr *trackModel) int64 { - if tr.track.Favorite { + t.intSort(func(tr *util.TrackListModel) int64 { + if tr.Track.Favorite { return 1 } return 0 @@ -509,41 +500,19 @@ func (t *Tracklist) onSelectTrack(idx int) { func (t *Tracklist) selectAddOrRemove(idx int) { t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() - t.tracks[idx].selected = !t.tracks[idx].selected + t.tracks[idx].Selected = !t.tracks[idx].Selected } func (t *Tracklist) selectTrack(idx int) { t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() - if t.tracks[idx].selected { - return - } - t.unselectAll() - t.tracks[idx].selected = true + util.SelectTrack(t.tracks, idx) } func (t *Tracklist) selectRange(idx int) { t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() - if t.tracks[idx].selected { - return - } - lastSelected := -1 - for i := len(t.tracks) - 1; i >= 0; i-- { - if t.tracks[i].selected { - lastSelected = i - break - } - } - if lastSelected < 0 { - t.tracks[idx].selected = true - return - } - from := minInt(idx, lastSelected) - to := maxInt(idx, lastSelected) - for i := from; i <= to; i++ { - t.tracks[i].selected = true - } + util.SelectTrackRange(t.tracks, idx) } func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) { @@ -667,32 +636,32 @@ func (t *Tracklist) onDownload(tracks []*mediaprovider.Track, downloadName strin } func (t *Tracklist) findTrackByID(id string) (*mediaprovider.Track, int) { - idx := sharedutil.Find(t.tracks, func(tr *trackModel) bool { - return tr.track.ID == id + idx := sharedutil.Find(t.tracks, func(tr *util.TrackListModel) bool { + return tr.Track.ID == id }) if idx >= 0 { - return t.tracks[idx].track, idx + return t.tracks[idx].Track, idx } return nil, -1 } -func (t *Tracklist) selectedTrackModels() []*trackModel { +func (t *Tracklist) selectedTrackModels() []*util.TrackListModel { t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() - return sharedutil.FilterSlice(t.tracks, func(tm *trackModel) bool { - return tm.selected + return sharedutil.FilterSlice(t.tracks, func(tm *util.TrackListModel) bool { + return tm.Selected }) } func (t *Tracklist) selectedTracks() []*mediaprovider.Track { - return sharedutil.MapSlice(t.selectedTrackModels(), func(tm *trackModel) *mediaprovider.Track { - return tm.track + return sharedutil.MapSlice(t.selectedTrackModels(), func(tm *util.TrackListModel) *mediaprovider.Track { + return tm.Track }) } func (t *Tracklist) SelectedTrackIDs() []string { - return sharedutil.MapSlice(t.selectedTrackModels(), func(tm *trackModel) string { - return tm.track.ID + return sharedutil.MapSlice(t.selectedTrackModels(), func(tm *util.TrackListModel) string { + return tm.Track.ID }) } @@ -752,15 +721,15 @@ type TrackRow struct { func NewTrackRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *TrackRow { t := &TrackRow{tracklist: tracklist, playingIcon: playingIcon} t.ExtendBaseWidget(t) - t.num = newTrailingAlignLabel() - t.name = newTruncatingRichText() + t.num = util.NewTrailingAlignLabel() + t.name = util.NewTruncatingRichText() t.artist = NewMultiHyperlink() t.artist.OnTapped = tracklist.onArtistTapped t.album = widget.NewHyperlink("", nil) t.album.Truncation = fyne.TextTruncateEllipsis t.album.OnTapped = func() { tracklist.onAlbumTapped(t.albumID) } - t.dur = newTrailingAlignLabel() - t.year = newTrailingAlignLabel() + t.dur = util.NewTrailingAlignLabel() + t.year = util.NewTrailingAlignLabel() favorite := NewFavoriteIcon() favorite.OnTapped = t.toggleFavorited t.favorite = container.NewCenter(favorite) @@ -768,45 +737,27 @@ func NewTrackRow(tracklist *Tracklist, playingIcon fyne.CanvasObject) *TrackRow t.rating.IsDisabled = t.tracklist.Options.DisableRating t.rating.StarSize = 16 t.rating.OnRatingChanged = t.setTrackRating - t.plays = newTrailingAlignLabel() - t.comment = newTruncatingLabel() - t.bitrate = newTrailingAlignLabel() - t.size = newTrailingAlignLabel() - t.path = newTruncatingLabel() + t.plays = util.NewTrailingAlignLabel() + t.comment = util.NewTruncatingLabel() + t.bitrate = util.NewTrailingAlignLabel() + t.size = util.NewTrailingAlignLabel() + t.path = util.NewTruncatingLabel() t.Content = container.New(tracklist.colLayout, t.num, t.name, t.artist, t.album, t.dur, t.year, t.favorite, t.rating, t.plays, t.comment, t.bitrate, t.size, t.path) return t } -func newTruncatingRichText() *widget.RichText { - rt := widget.NewRichTextWithText("") - rt.Truncation = fyne.TextTruncateEllipsis - return rt -} - -func newTruncatingLabel() *widget.Label { - rt := widget.NewLabel("") - rt.Truncation = fyne.TextTruncateEllipsis - return rt -} - -func newTrailingAlignLabel() *widget.Label { - rt := widget.NewLabel("") - rt.Alignment = fyne.TextAlignTrailing - return rt -} - -func (t *TrackRow) Update(tm *trackModel, rowNum int) { +func (t *TrackRow) Update(tm *util.TrackListModel, rowNum int) { changed := false - if tm.selected != t.Selected { - t.Selected = tm.selected + if tm.Selected != t.Selected { + t.Selected = tm.Selected changed = true } // Update info that can change if this row is bound to // a new track (*mediaprovider.Track) - tr := tm.track + tr := tm.Track if tr.ID != t.trackID { t.EnsureUnfocused() t.trackID = tr.ID @@ -925,17 +876,3 @@ func (t *TrackRow) TappedSecondary(e *fyne.PointEvent) { t.OnTappedSecondary(e, t.ListItemID) } } - -func minInt(a, b int) int { - if a < b { - return a - } - return b -} - -func maxInt(a, b int) int { - if a > b { - return a - } - return b -} From c2c65c3d76ac91f6ae308491214e483c9d605ef7 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 2 Mar 2024 12:29:07 -0800 Subject: [PATCH 10/28] beginning work on PlayQueueList --- ui/widgets/tracklist.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 5d77102..cd052c0 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -810,7 +810,7 @@ func (t *TrackRow) Update(tm *util.TrackListModel, rowNum int) { t.name.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = isPlaying if isPlaying { - t.Content.(*fyne.Container).Objects[0] = container.NewCenter(t.playingIcon) + t.Content.(*fyne.Container).Objects[0] = t.playingIcon } else { t.Content.(*fyne.Container).Objects[0] = t.num } From a75222d381332623a55b48c863e769efcc74a7a5 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 2 Mar 2024 13:34:09 -0800 Subject: [PATCH 11/28] more work --- ui/util/tracklistutil.go | 11 ++- ui/widgets/playqueuelist.go | 153 ++++++++++++++++++++++++++++++++++++ ui/widgets/tracklist.go | 10 +-- 3 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 ui/widgets/playqueuelist.go diff --git a/ui/util/tracklistutil.go b/ui/util/tracklistutil.go index 00f7de8..7a1b571 100644 --- a/ui/util/tracklistutil.go +++ b/ui/util/tracklistutil.go @@ -1,12 +1,21 @@ package util -import "github.com/dweymouth/supersonic/backend/mediaprovider" +import ( + "github.com/dweymouth/supersonic/backend/mediaprovider" + "github.com/dweymouth/supersonic/sharedutil" +) type TrackListModel struct { Track *mediaprovider.Track Selected bool } +func ToTrackListModels(trs []*mediaprovider.Track) []*TrackListModel { + return sharedutil.MapSlice(trs, func(tr *mediaprovider.Track) *TrackListModel { + return &TrackListModel{Track: tr, Selected: false} + }) +} + func SelectTrack(tracks []*TrackListModel, idx int) { if tracks[idx].Selected { return diff --git a/ui/widgets/playqueuelist.go b/ui/widgets/playqueuelist.go new file mode 100644 index 0000000..179a2ff --- /dev/null +++ b/ui/widgets/playqueuelist.go @@ -0,0 +1,153 @@ +package widgets + +import ( + "sync" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/widget" + "github.com/dweymouth/supersonic/ui/layouts" + "github.com/dweymouth/supersonic/ui/os" + "github.com/dweymouth/supersonic/ui/theme" + "github.com/dweymouth/supersonic/ui/util" +) + +type PlayQueueList struct { + FocusList + + OnShowArtistPage func(artistID string) + OnPlayTrackAt func(idx int) + + nowPlayingID string + colLayout *layouts.ColumnsLayout + + tracksMutex sync.Mutex + tracks []*util.TrackListModel +} + +func (t *PlayQueueList) onArtistTapped(artistID string) { + if t.OnShowArtistPage != nil { + t.OnShowArtistPage(artistID) + } +} + +func (p *PlayQueueList) onPlayTrackAt(idx int) { + if p.OnPlayTrackAt != nil { + p.OnPlayTrackAt(idx) + } +} + +func (p *PlayQueueList) onSelectTrack(idx int) { + if d, ok := fyne.CurrentApp().Driver().(desktop.Driver); ok { + mod := d.CurrentKeyModifiers() + if mod&os.ControlModifier != 0 { + p.selectAddOrRemove(idx) + } else if mod&fyne.KeyModifierShift != 0 { + p.selectRange(idx) + } else { + p.selectTrack(idx) + } + } else { + p.selectTrack(idx) + } + p.Refresh() +} + +func (p *PlayQueueList) selectTrack(idx int) { + p.tracksMutex.Lock() + defer p.tracksMutex.Unlock() + util.SelectTrack(p.tracks, idx) +} + +func (p *PlayQueueList) selectAddOrRemove(idx int) { + p.tracksMutex.Lock() + defer p.tracksMutex.Unlock() + p.tracks[idx].Selected = !p.tracks[idx].Selected +} + +func (p *PlayQueueList) selectRange(idx int) { + p.tracksMutex.Lock() + defer p.tracksMutex.Unlock() + util.SelectTrackRange(p.tracks, idx) +} + +type PlayQueueListRow struct { + FocusListRowBase + + playQueueList *PlayQueueList + trackID string + isPlaying bool + + playingIcon fyne.CanvasObject + num *widget.Label + cover *ImagePlaceholder + title *widget.Label + artist *MultiHyperlink + time *widget.Label +} + +func NewPlayQueueListRow(playQueueList *PlayQueueList, playingIcon fyne.CanvasObject) *PlayQueueListRow { + p := &PlayQueueListRow{ + playingIcon: playingIcon, + playQueueList: playQueueList, + num: widget.NewLabel(""), + cover: NewImagePlaceholder(theme.TracksIcon, 64), + title: util.NewTruncatingLabel(), + artist: NewMultiHyperlink(), + time: util.NewTrailingAlignLabel(), + } + p.artist.OnTapped = playQueueList.onArtistTapped + p.OnDoubleTapped = func() { + playQueueList.onPlayTrackAt(p.ItemID()) + } + p.OnTapped = func() { + playQueueList.onSelectTrack(p.ItemID()) + } + //p.title.TextStyle.Bold = true + p.ExtendBaseWidget(p) + p.Content = container.New(playQueueList.colLayout, + container.NewCenter(p.num), + p.cover, + container.New(&layouts.VboxCustomPadding{ExtraPad: -15}, + p.title, p.artist), + container.NewCenter(p.time), + ) + return p +} + +func (p *PlayQueueListRow) Update(tm *util.TrackListModel, rowNum int) { + changed := false + if tm.Selected != p.Selected { + p.Selected = tm.Selected + changed = true + } + + // Update info that can change if this row is bound to + // a new track (*mediaprovider.Track) + tr := tm.Track + if tr.ID != p.trackID { + p.EnsureUnfocused() + p.trackID = tr.ID + p.artist.BuildSegments(tr.ArtistNames, tr.ArtistIDs) + p.time.Text = util.SecondsToTimeString(float64(tr.Duration)) + changed = true + } + + // Render whether track is playing or not + if isPlaying := p.playQueueList.nowPlayingID == tr.ID; isPlaying != p.isPlaying { + p.isPlaying = isPlaying + p.title.TextStyle.Bold = isPlaying + + if isPlaying { + p.Content.(*fyne.Container).Objects[0] = p.playingIcon + } else { + p.Content.(*fyne.Container).Objects[0] = p.num + } + changed = true + } + + if changed { + p.Refresh() + } +} diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index cd052c0..1452a3b 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -323,7 +323,7 @@ func (t *Tracklist) _setTracks(trs []*mediaprovider.Track) { if t.list != nil { t.list.ClearItemForIDMap() } - t.tracksOrigOrder = toTrackModels(trs) + t.tracksOrigOrder = util.ToTrackListModels(trs) t.doSortTracks() } @@ -340,7 +340,7 @@ func (t *Tracklist) GetTracks() []*mediaprovider.Track { func (t *Tracklist) AppendTracks(trs []*mediaprovider.Track) { t.tracksMutex.Lock() defer t.tracksMutex.Unlock() - t.tracksOrigOrder = append(t.tracks, toTrackModels(trs)...) + t.tracksOrigOrder = append(t.tracks, util.ToTrackListModels(trs)...) t.doSortTracks() } @@ -388,12 +388,6 @@ func (t *Tracklist) Refresh() { t.BaseWidget.Refresh() } -func toTrackModels(trs []*mediaprovider.Track) []*util.TrackListModel { - return sharedutil.MapSlice(trs, func(tr *mediaprovider.Track) *util.TrackListModel { - return &util.TrackListModel{Track: tr, Selected: false} - }) -} - // do nothing Tapped handler so that tapping the separator between rows // doesn't fall through to the page (which calls UnselectAll on tracklist) func (t *Tracklist) Tapped(*fyne.PointEvent) {} From b8ab480294d3583ffe0fccb7167837e7d0c6f68b Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 2 Mar 2024 16:01:14 -0800 Subject: [PATCH 12/28] fullscreen play queue renders correctly --- ui/browsing/fullscreenpage.go | 35 ++++---- ui/util/tracklistutil.go | 10 +++ ui/widgets/playqueuelist.go | 148 ++++++++++++++++++++++++++++++---- ui/widgets/tracklist.go | 20 ++--- 4 files changed, 170 insertions(+), 43 deletions(-) diff --git a/ui/browsing/fullscreenpage.go b/ui/browsing/fullscreenpage.go index e992eb6..b652653 100644 --- a/ui/browsing/fullscreenpage.go +++ b/ui/browsing/fullscreenpage.go @@ -25,10 +25,12 @@ type FullscreenPage struct { fullscreenPageState queue []*mediaprovider.Track + queueList *widgets.PlayQueueList imageLoadCancel context.CancelFunc card *widgets.LargeNowPlayingCard nowPlayingID string albumID string + container *fyne.Container } type fullscreenPageState struct { @@ -66,25 +68,28 @@ func NewFullscreenPage( a.contr.SetTrackRatings([]string{a.nowPlayingID}, rating) } + a.queueList = widgets.NewPlayQueueList(a.im) + paddedLayout := &layouts.PercentPadLayout{ + LeftRightObjectPercent: .8, + TopBottomObjectPercent: .8, + } + a.container = container.NewGridWithColumns(2, + container.New(paddedLayout, a.card), + container.New(paddedLayout, + util.AddHeaderBackground( + container.NewAppTabs( + container.NewTabItem("Play Queue", + container.NewBorder(layout.NewSpacer(), nil, nil, nil, a.queueList)), + container.NewTabItem("Lyrics", layout.NewSpacer()), + ))), + ) + a.Reload() return a } func (a *FullscreenPage) CreateRenderer() fyne.WidgetRenderer { - paddedLayout := &layouts.PercentPadLayout{ - LeftRightObjectPercent: .8, - TopBottomObjectPercent: .8, - } - container := container.NewGridWithColumns(2, - container.New(paddedLayout, a.card), - container.New(paddedLayout, - util.AddHeaderBackground( - container.NewAppTabs( - container.NewTabItem("Up Next", layout.NewSpacer()), - container.NewTabItem("Lyrics", layout.NewSpacer()), - ))), - ) - return widget.NewSimpleRenderer(container) + return widget.NewSimpleRenderer(a.container) } func (a *FullscreenPage) Save() SavedPage { @@ -115,10 +120,12 @@ func (a *FullscreenPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Tr a.card.SetCoverImage(img) } }) + a.queueList.SetNowPlaying(song.ID) } func (a *FullscreenPage) Reload() { a.queue = a.pm.GetPlayQueue() + a.queueList.SetTracks(a.queue) } func (s *fullscreenPageState) Restore() Page { diff --git a/ui/util/tracklistutil.go b/ui/util/tracklistutil.go index 7a1b571..675c6e9 100644 --- a/ui/util/tracklistutil.go +++ b/ui/util/tracklistutil.go @@ -58,6 +58,16 @@ func SelectTrackRange(tracks []*TrackListModel, idx int) { } } +func FindTrackByID(tracks []*TrackListModel, id string) (*mediaprovider.Track, int) { + idx := sharedutil.Find(tracks, func(tr *TrackListModel) bool { + return tr.Track.ID == id + }) + if idx >= 0 { + return tracks[idx].Track, idx + } + return nil, -1 +} + func minInt(a, b int) int { if a < b { return a diff --git a/ui/widgets/playqueuelist.go b/ui/widgets/playqueuelist.go index 179a2ff..062ab57 100644 --- a/ui/widgets/playqueuelist.go +++ b/ui/widgets/playqueuelist.go @@ -1,20 +1,30 @@ package widgets import ( + "image" + "strconv" "sync" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" + "github.com/dweymouth/supersonic/backend" + "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/os" - "github.com/dweymouth/supersonic/ui/theme" + myTheme "github.com/dweymouth/supersonic/ui/theme" "github.com/dweymouth/supersonic/ui/util" ) +const thumbnailSize = 60 + type PlayQueueList struct { - FocusList + widget.BaseWidget + + list *FocusList OnShowArtistPage func(artistID string) OnPlayTrackAt func(idx int) @@ -22,10 +32,84 @@ type PlayQueueList struct { nowPlayingID string colLayout *layouts.ColumnsLayout - tracksMutex sync.Mutex + tracksMutex sync.RWMutex tracks []*util.TrackListModel } +func NewPlayQueueList(im *backend.ImageManager) *PlayQueueList { + p := &PlayQueueList{} + p.ExtendBaseWidget(p) + + // #, Cover, Title/Artist, Time + p.colLayout = layouts.NewColumnsLayout([]float32{40, thumbnailSize, -1, 60}) + + playIconResource := theme.NewThemedResource(theme.MediaPlayIcon()) + playIconResource.ColorName = theme.ColorNamePrimary + playIconImg := canvas.NewImageFromResource(playIconResource) + playIconImg.FillMode = canvas.ImageFillContain + playIconImg.SetMinSize(fyne.NewSquareSize(theme.IconInlineSize() * 1.5)) + + playingIcon := container.NewCenter(playIconImg) + + p.list = NewFocusList( + p.lenTracks, + func() fyne.CanvasObject { + return NewPlayQueueListRow(p, im, playingIcon) + }, + func(itemID widget.ListItemID, item fyne.CanvasObject) { + p.tracksMutex.RLock() + // we could have removed tracks from the list in between + // Fyne calling the length callback and this update callback + // so the itemID may be out of bounds. if so, do nothing. + if itemID >= len(p.tracks) { + p.tracksMutex.RUnlock() + return + } + model := p.tracks[itemID] + p.tracksMutex.RUnlock() + + tr := item.(*PlayQueueListRow) + p.list.SetItemForID(itemID, tr) + if tr.trackID != model.Track.ID || tr.ListItemID != itemID { + tr.ListItemID = itemID + } + tr.Update(model, itemID+1) + }, + ) + + return p +} + +func (p *PlayQueueList) SetTracks(trs []*mediaprovider.Track) { + p.tracksMutex.Lock() + p.list.ClearItemForIDMap() + p.tracks = util.ToTrackListModels(trs) + p.tracksMutex.Unlock() + p.Refresh() +} + +// Sets the currently playing track ID and updates the list rendering +func (p *PlayQueueList) SetNowPlaying(trackID string) { + prevNowPlaying := p.nowPlayingID + p.tracksMutex.RLock() + trPrev, idxPrev := util.FindTrackByID(p.tracks, prevNowPlaying) + tr, idx := util.FindTrackByID(p.tracks, trackID) + p.tracksMutex.RUnlock() + p.nowPlayingID = trackID + if trPrev != nil { + p.list.RefreshItem(idxPrev) + } + if tr != nil { + p.list.RefreshItem(idx) + } +} + +func (p *PlayQueueList) lenTracks() int { + p.tracksMutex.RLock() + defer p.tracksMutex.RUnlock() + return len(p.tracks) +} + func (t *PlayQueueList) onArtistTapped(artistID string) { if t.OnShowArtistPage != nil { t.OnShowArtistPage(artistID) @@ -55,26 +139,37 @@ func (p *PlayQueueList) onSelectTrack(idx int) { } func (p *PlayQueueList) selectTrack(idx int) { - p.tracksMutex.Lock() - defer p.tracksMutex.Unlock() + p.tracksMutex.RLock() + defer p.tracksMutex.RUnlock() util.SelectTrack(p.tracks, idx) } func (p *PlayQueueList) selectAddOrRemove(idx int) { - p.tracksMutex.Lock() - defer p.tracksMutex.Unlock() + p.tracksMutex.RLock() + defer p.tracksMutex.RUnlock() p.tracks[idx].Selected = !p.tracks[idx].Selected } func (p *PlayQueueList) selectRange(idx int) { - p.tracksMutex.Lock() - defer p.tracksMutex.Unlock() + p.tracksMutex.RLock() + defer p.tracksMutex.RUnlock() util.SelectTrackRange(p.tracks, idx) } +func (p *PlayQueueList) onShowContextMenu(e *fyne.PointEvent, trackIdx int) { + +} + +func (p *PlayQueueList) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(p.list) +} + type PlayQueueListRow struct { FocusListRowBase + OnTappedSecondary func(e *fyne.PointEvent, trackIdx int) + + imageLoader util.ThumbnailLoader playQueueList *PlayQueueList trackID string isPlaying bool @@ -87,16 +182,18 @@ type PlayQueueListRow struct { time *widget.Label } -func NewPlayQueueListRow(playQueueList *PlayQueueList, playingIcon fyne.CanvasObject) *PlayQueueListRow { +func NewPlayQueueListRow(playQueueList *PlayQueueList, im *backend.ImageManager, playingIcon fyne.CanvasObject) *PlayQueueListRow { p := &PlayQueueListRow{ playingIcon: playingIcon, playQueueList: playQueueList, num: widget.NewLabel(""), - cover: NewImagePlaceholder(theme.TracksIcon, 64), + cover: NewImagePlaceholder(myTheme.TracksIcon, thumbnailSize), title: util.NewTruncatingLabel(), artist: NewMultiHyperlink(), time: util.NewTrailingAlignLabel(), } + p.ExtendBaseWidget(p) + p.artist.OnTapped = playQueueList.onArtistTapped p.OnDoubleTapped = func() { playQueueList.onPlayTrackAt(p.ItemID()) @@ -104,8 +201,18 @@ func NewPlayQueueListRow(playQueueList *PlayQueueList, playingIcon fyne.CanvasOb p.OnTapped = func() { playQueueList.onSelectTrack(p.ItemID()) } - //p.title.TextStyle.Bold = true - p.ExtendBaseWidget(p) + p.OnTappedSecondary = playQueueList.onShowContextMenu + p.OnFocusNeighbor = func(up bool) { + playQueueList.list.FocusNeighbor(p.ItemID(), up) + } + + p.imageLoader = util.NewThumbnailLoader(im, func(i image.Image) { + p.cover.SetImage(i, false) + }) + p.imageLoader.OnBeforeLoad = func() { + p.cover.SetImage(nil, false) + } + p.Content = container.New(playQueueList.colLayout, container.NewCenter(p.num), p.cover, @@ -116,6 +223,12 @@ func NewPlayQueueListRow(playQueueList *PlayQueueList, playingIcon fyne.CanvasOb return p } +func (p *PlayQueueListRow) TappedSecondary(e *fyne.PointEvent) { + if p.OnTappedSecondary != nil { + p.OnTappedSecondary(e, p.ListItemID) + } +} + func (p *PlayQueueListRow) Update(tm *util.TrackListModel, rowNum int) { changed := false if tm.Selected != p.Selected { @@ -123,12 +236,19 @@ func (p *PlayQueueListRow) Update(tm *util.TrackListModel, rowNum int) { changed = true } + if num := strconv.Itoa(rowNum); p.num.Text != num { + p.num.Text = num + changed = true + } + // Update info that can change if this row is bound to // a new track (*mediaprovider.Track) tr := tm.Track if tr.ID != p.trackID { + p.imageLoader.Load(tm.Track.CoverArtID) p.EnsureUnfocused() p.trackID = tr.ID + p.title.Text = tr.Name p.artist.BuildSegments(tr.ArtistNames, tr.ArtistIDs) p.time.Text = util.SecondsToTimeString(float64(tr.Duration)) changed = true @@ -142,7 +262,7 @@ func (p *PlayQueueListRow) Update(tm *util.TrackListModel, rowNum int) { if isPlaying { p.Content.(*fyne.Container).Objects[0] = p.playingIcon } else { - p.Content.(*fyne.Container).Objects[0] = p.num + p.Content.(*fyne.Container).Objects[0] = container.NewCenter(p.num) } changed = true } diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 1452a3b..4b17503 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -279,8 +279,8 @@ func (t *Tracklist) SetSorting(sorting TracklistSort) { func (t *Tracklist) SetNowPlaying(trackID string) { prevNowPlaying := t.nowPlayingID t.tracksMutex.RLock() - trPrev, idxPrev := t.findTrackByID(prevNowPlaying) - tr, idx := t.findTrackByID(trackID) + trPrev, idxPrev := util.FindTrackByID(t.tracks, prevNowPlaying) + tr, idx := util.FindTrackByID(t.tracks, trackID) t.tracksMutex.RUnlock() t.nowPlayingID = trackID if trPrev != nil { @@ -294,7 +294,7 @@ func (t *Tracklist) SetNowPlaying(trackID string) { // Increments the play count of the given track and updates the list rendering func (t *Tracklist) IncrementPlayCount(trackID string) { t.tracksMutex.RLock() - tr, idx := t.findTrackByID(trackID) + tr, idx := util.FindTrackByID(t.tracks, trackID) t.tracksMutex.RUnlock() if tr != nil { tr.PlayCount += 1 @@ -572,7 +572,7 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) { func (t *Tracklist) onSetFavorite(trackID string, fav bool) { t.tracksMutex.RLock() - tr, _ := t.findTrackByID(trackID) + tr, _ := util.FindTrackByID(t.tracks, trackID) t.tracksMutex.RUnlock() t.onSetFavorites([]*mediaprovider.Track{tr}, fav, false) } @@ -593,7 +593,7 @@ func (t *Tracklist) onSetFavorites(tracks []*mediaprovider.Track, fav bool, need func (t *Tracklist) onSetRating(trackID string, rating int) { // update our own track model t.tracksMutex.RLock() - tr, _ := t.findTrackByID(trackID) + tr, _ := util.FindTrackByID(t.tracks, trackID) t.tracksMutex.RUnlock() t.onSetRatings([]*mediaprovider.Track{tr}, rating, false) } @@ -629,16 +629,6 @@ func (t *Tracklist) onDownload(tracks []*mediaprovider.Track, downloadName strin } } -func (t *Tracklist) findTrackByID(id string) (*mediaprovider.Track, int) { - idx := sharedutil.Find(t.tracks, func(tr *util.TrackListModel) bool { - return tr.Track.ID == id - }) - if idx >= 0 { - return t.tracks[idx].Track, idx - } - return nil, -1 -} - func (t *Tracklist) selectedTrackModels() []*util.TrackListModel { t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() From a458f14327495f3d30607fa27147eec58e2ff484 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 2 Mar 2024 17:10:32 -0800 Subject: [PATCH 13/28] don't crash in fullscreen page when stopping (next track = nil) --- sharedutil/sharedutil.go | 7 +++++++ ui/browsing/fullscreenpage.go | 9 +++++++-- ui/widgets/largenowplayingcard.go | 22 +++++++++++++--------- ui/widgets/playqueuelist.go | 8 +++++--- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/sharedutil/sharedutil.go b/sharedutil/sharedutil.go index d05a909..cce0323 100644 --- a/sharedutil/sharedutil.go +++ b/sharedutil/sharedutil.go @@ -102,6 +102,13 @@ func TrackIDOrEmptyStr(track *mediaprovider.Track) string { return track.ID } +func AlbumIDOrEmptyStr(track *mediaprovider.Track) string { + if track == nil { + return "" + } + return track.ID +} + func TracksToIDs(tracks []*mediaprovider.Track) []string { return MapSlice(tracks, func(tr *mediaprovider.Track) string { return tr.ID diff --git a/ui/browsing/fullscreenpage.go b/ui/browsing/fullscreenpage.go index b652653..1e4e5d1 100644 --- a/ui/browsing/fullscreenpage.go +++ b/ui/browsing/fullscreenpage.go @@ -110,9 +110,15 @@ func (a *FullscreenPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Tr if a.imageLoadCancel != nil { a.imageLoadCancel() } - a.albumID = song.AlbumID a.nowPlayingID = sharedutil.TrackIDOrEmptyStr(song) + a.queueList.SetNowPlaying(a.nowPlayingID) + + a.albumID = sharedutil.AlbumIDOrEmptyStr(song) a.card.Update(song) + if song == nil { + a.card.SetCoverImage(nil) + return + } a.imageLoadCancel = a.im.GetFullSizeCoverArtAsync(song.CoverArtID, func(img image.Image, err error) { if err != nil { log.Printf("error loading cover art: %v\n", err) @@ -120,7 +126,6 @@ func (a *FullscreenPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Tr a.card.SetCoverImage(img) } }) - a.queueList.SetNowPlaying(song.ID) } func (a *FullscreenPage) Reload() { diff --git a/ui/widgets/largenowplayingcard.go b/ui/widgets/largenowplayingcard.go index abfb9e9..743a19b 100644 --- a/ui/widgets/largenowplayingcard.go +++ b/ui/widgets/largenowplayingcard.go @@ -63,8 +63,6 @@ func NewLargeNowPlayingCard() *LargeNowPlayingCard { n.trackName.Segments[0].(*widget.TextSegment).Style.SizeName = theme.SizeNameSubHeadingText n.trackName.Truncation = fyne.TextTruncateEllipsis - n.trackName.Hidden = true - n.albumName.Hidden = true n.albumName.Truncation = fyne.TextTruncateEllipsis n.trackName.Truncation = fyne.TextTruncateEllipsis n.albumName.OnTapped = n.onAlbumNameTapped @@ -100,13 +98,19 @@ func (n *LargeNowPlayingCard) onSetRating(rating int) { } func (n *LargeNowPlayingCard) Update(track *mediaprovider.Track) { - n.trackName.Segments[0].(*widget.TextSegment).Text = track.Name - n.trackName.Hidden = track.Name == "" - n.artistName.BuildSegments(track.ArtistNames, track.ArtistIDs) - n.albumName.Text = track.Album - n.albumName.Hidden = track.Album == "" - n.rating.Rating = track.Rating - n.favorite.Favorite = track.Favorite + if track != nil { + n.trackName.Segments[0].(*widget.TextSegment).Text = track.Name + n.artistName.BuildSegments(track.ArtistNames, track.ArtistIDs) + n.albumName.Text = track.Album + n.rating.Rating = track.Rating + n.favorite.Favorite = track.Favorite + } else { + n.trackName.Segments[0].(*widget.TextSegment).Text = "" + n.artistName.BuildSegments([]string{}, []string{}) + n.albumName.Text = "" + n.rating.Rating = 0 + n.favorite.Favorite = false + } n.Refresh() } diff --git a/ui/widgets/playqueuelist.go b/ui/widgets/playqueuelist.go index 062ab57..41eee8d 100644 --- a/ui/widgets/playqueuelist.go +++ b/ui/widgets/playqueuelist.go @@ -9,6 +9,7 @@ import ( "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "github.com/dweymouth/supersonic/backend" @@ -19,7 +20,7 @@ import ( "github.com/dweymouth/supersonic/ui/util" ) -const thumbnailSize = 60 +const thumbnailSize = 52 type PlayQueueList struct { widget.BaseWidget @@ -41,7 +42,8 @@ func NewPlayQueueList(im *backend.ImageManager) *PlayQueueList { p.ExtendBaseWidget(p) // #, Cover, Title/Artist, Time - p.colLayout = layouts.NewColumnsLayout([]float32{40, thumbnailSize, -1, 60}) + coverWidth := NewPlayQueueListRow(p, im, layout.NewSpacer()).cover.MinSize().Width + p.colLayout = layouts.NewColumnsLayout([]float32{40, coverWidth, -1, 60}) playIconResource := theme.NewThemedResource(theme.MediaPlayIcon()) playIconResource.ColorName = theme.ColorNamePrimary @@ -215,7 +217,7 @@ func NewPlayQueueListRow(playQueueList *PlayQueueList, im *backend.ImageManager, p.Content = container.New(playQueueList.colLayout, container.NewCenter(p.num), - p.cover, + container.NewPadded(p.cover), container.New(&layouts.VboxCustomPadding{ExtraPad: -15}, p.title, p.artist), container.NewCenter(p.time), From 9f16e2ebd9d20e839ee6335d264333561b250356 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 2 Mar 2024 17:18:49 -0800 Subject: [PATCH 14/28] connect actions --- ui/browsing/fullscreenpage.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui/browsing/fullscreenpage.go b/ui/browsing/fullscreenpage.go index 1e4e5d1..a48476a 100644 --- a/ui/browsing/fullscreenpage.go +++ b/ui/browsing/fullscreenpage.go @@ -69,6 +69,12 @@ func NewFullscreenPage( } a.queueList = widgets.NewPlayQueueList(a.im) + a.queueList.OnPlayTrackAt = func(tracknum int) { + _ = a.pm.PlayTrackAt(tracknum) + } + a.queueList.OnShowArtistPage = func(artistID string) { + a.contr.NavigateTo(controller.ArtistRoute(artistID)) + } paddedLayout := &layouts.PercentPadLayout{ LeftRightObjectPercent: .8, TopBottomObjectPercent: .8, From 825f4325ed3bb5aa194863d62c785cd84605c551 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 2 Mar 2024 17:25:16 -0800 Subject: [PATCH 15/28] pool fullscreenpage for reuse when navigating --- ui/browsing/fullscreenpage.go | 5 +++++ ui/util/widgetpool.go | 1 + 2 files changed, 6 insertions(+) diff --git a/ui/browsing/fullscreenpage.go b/ui/browsing/fullscreenpage.go index a48476a..78e85cb 100644 --- a/ui/browsing/fullscreenpage.go +++ b/ui/browsing/fullscreenpage.go @@ -103,6 +103,7 @@ func (a *FullscreenPage) Save() SavedPage { a.imageLoadCancel() } nps := a.fullscreenPageState + a.pool.Release(util.WidgetTypeFullscreenPage, a) return &nps } @@ -140,5 +141,9 @@ func (a *FullscreenPage) Reload() { } func (s *fullscreenPageState) Restore() Page { + if page := s.pool.Obtain(util.WidgetTypeFullscreenPage).(*FullscreenPage); page != nil { + page.Reload() + return page + } return NewFullscreenPage(s.contr, s.pool, s.im, s.pm, s.canRate) } diff --git a/ui/util/widgetpool.go b/ui/util/widgetpool.go index 5a81d12..0db2ef0 100644 --- a/ui/util/widgetpool.go +++ b/ui/util/widgetpool.go @@ -15,6 +15,7 @@ const ( WidgetTypeGridView WidgetTypePlaylistPageHeader WidgetTypeTracklist + WidgetTypeFullscreenPage // keep at bottom numWidgetTypes From 1e010d5452fd5b72e72236e5ece43e43e508d5a8 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 2 Mar 2024 17:31:08 -0800 Subject: [PATCH 16/28] use fast scaling mode for play queue images --- ui/widgets/playqueuelist.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/widgets/playqueuelist.go b/ui/widgets/playqueuelist.go index 41eee8d..81ac4cc 100644 --- a/ui/widgets/playqueuelist.go +++ b/ui/widgets/playqueuelist.go @@ -196,6 +196,7 @@ func NewPlayQueueListRow(playQueueList *PlayQueueList, im *backend.ImageManager, } p.ExtendBaseWidget(p) + p.cover.ScaleMode = canvas.ImageScaleFastest p.artist.OnTapped = playQueueList.onArtistTapped p.OnDoubleTapped = func() { playQueueList.onPlayTrackAt(p.ItemID()) From 013204727d9a9e4e7b24fa78b4f7aa227f5b58e2 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 3 Mar 2024 10:19:14 -0800 Subject: [PATCH 17/28] fix album link bug --- sharedutil/sharedutil.go | 2 +- ui/widgets/largenowplayingcard.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sharedutil/sharedutil.go b/sharedutil/sharedutil.go index cce0323..b1c60d3 100644 --- a/sharedutil/sharedutil.go +++ b/sharedutil/sharedutil.go @@ -106,7 +106,7 @@ func AlbumIDOrEmptyStr(track *mediaprovider.Track) string { if track == nil { return "" } - return track.ID + return track.AlbumID } func TracksToIDs(tracks []*mediaprovider.Track) []string { diff --git a/ui/widgets/largenowplayingcard.go b/ui/widgets/largenowplayingcard.go index 743a19b..f478fa1 100644 --- a/ui/widgets/largenowplayingcard.go +++ b/ui/widgets/largenowplayingcard.go @@ -50,8 +50,8 @@ func NewLargeNowPlayingCard() *LargeNowPlayingCard { n.Content = n.cover n.Caption = container.New(&layouts.VboxCustomPadding{ExtraPad: -13}, n.trackName, - n.albumName, n.artistName, + n.albumName, container.NewHBox( layout.NewSpacer(), n.favorite, From 9b72ff9d172fcbfa06a6247b7e94d1c359d13774 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 3 Mar 2024 10:38:12 -0800 Subject: [PATCH 18/28] add OnPlayQueueChange hook --- backend/playbackengine.go | 10 ++++++++++ backend/playbackmanager.go | 5 +++++ ui/browsing/browsingpane.go | 14 ++++++++++++++ ui/browsing/fullscreenpage.go | 4 ++++ 4 files changed, 33 insertions(+) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index ebc8419..04eaed9 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -61,6 +61,7 @@ type playbackEngine struct { onStopped []func() onPlaying []func() onPlayerChange []func() + onQueueChange []func() } func NewPlaybackEngine( @@ -216,15 +217,21 @@ func (p *playbackEngine) LoadTracks(tracks []*mediaprovider.Track, appendToQueue if needToSetNext { p.setNextTrack(p.nowPlayingIdx + 1) } + + p.invokeNoArgCallbacks(p.onQueueChange) return nil } // Stop playback and clear the play queue. func (p *playbackEngine) StopAndClearPlayQueue() { + changed := len(p.playQueue) > 0 p.player.Stop() p.doUpdateTimePos() p.playQueue = nil p.nowPlayingIdx = -1 + if changed { + p.invokeNoArgCallbacks(p.onQueueChange) + } } func (p *playbackEngine) GetPlayQueue() []*mediaprovider.Track { @@ -273,6 +280,7 @@ func (p *playbackEngine) UpdatePlayQueue(tracks []*mediaprovider.Track) error { p.setNextTrackAfterQueueUpdate() } + p.invokeNoArgCallbacks(p.onQueueChange) return nil } @@ -321,6 +329,8 @@ func (p *playbackEngine) RemoveTracksFromQueue(trackIDs []string) { p.setNextTrack(-1) } } + + p.invokeNoArgCallbacks(p.onQueueChange) } func (p *playbackEngine) SetReplayGainOptions(config ReplayGainConfig) { diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index e5288a5..ae1c70d 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -74,6 +74,11 @@ func (p *PlaybackManager) OnVolumeChange(cb func(int)) { p.engine.onVolumeChange = append(p.engine.onVolumeChange, cb) } +// Registers a callback that is notified whenever the play queue changes. +func (p *PlaybackManager) OnQueueChange(cb func()) { + p.engine.onQueueChange = append(p.engine.onQueueChange, cb) +} + // Registers a callback that is notified whenever the player has been seeked. func (p *PlaybackManager) OnSeek(cb func()) { p.engine.onSeek = append(p.engine.onSeek, cb) diff --git a/ui/browsing/browsingpane.go b/ui/browsing/browsingpane.go index fc275b6..6168c15 100644 --- a/ui/browsing/browsingpane.go +++ b/ui/browsing/browsingpane.go @@ -49,6 +49,10 @@ type CanShowPlayTime interface { OnPlayTimeUpdate(curTime, totalTime float64) } +type CanShowPlayQueue interface { + OnPlayQueueChange() +} + type BrowsingPane struct { widget.BaseWidget @@ -80,6 +84,7 @@ func NewBrowsingPane(app *backend.App, contr *controller.Controller) *BrowsingPa b.reload = widget.NewButtonWithIcon("", theme.ViewRefreshIcon(), b.Reload) b.app.PlaybackManager.OnSongChange(b.onSongChange) b.app.PlaybackManager.OnPlayTimeUpdate(b.onPlayTimeUpdate) + b.app.PlaybackManager.OnQueueChange(b.onQueueChange) bkgrnd := myTheme.NewThemedRectangle(myTheme.ColorNamePageBackground) b.pageContainer = container.NewStack(bkgrnd, layout.NewSpacer()) b.settingsBtn = widget.NewButtonWithIcon("", theme.SettingsIcon(), func() { @@ -222,6 +227,15 @@ func (b *BrowsingPane) onPlayTimeUpdate(cur, total float64) { } } +func (b *BrowsingPane) onQueueChange() { + if b.curPage == nil { + return + } + if p, ok := b.curPage.(CanShowPlayQueue); ok { + p.OnPlayQueueChange() + } +} + func (b *BrowsingPane) addPageToHistory(p Page, truncate bool) { if truncate { // allow garbage collection of pages that will be removed from the history diff --git a/ui/browsing/fullscreenpage.go b/ui/browsing/fullscreenpage.go index 78e85cb..65b6ccf 100644 --- a/ui/browsing/fullscreenpage.go +++ b/ui/browsing/fullscreenpage.go @@ -135,6 +135,10 @@ func (a *FullscreenPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Tr }) } +func (a *FullscreenPage) OnPlayQueueChange() { + a.Reload() +} + func (a *FullscreenPage) Reload() { a.queue = a.pm.GetPlayQueue() a.queueList.SetTracks(a.queue) From d8f217234114c1dc354c972c201e6665ffd7a203 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 3 Mar 2024 10:42:09 -0800 Subject: [PATCH 19/28] use fastest image scale mode for now playing cards --- ui/widgets/largenowplayingcard.go | 2 ++ ui/widgets/nowplayingcard.go | 1 + 2 files changed, 3 insertions(+) diff --git a/ui/widgets/largenowplayingcard.go b/ui/widgets/largenowplayingcard.go index f478fa1..fcea659 100644 --- a/ui/widgets/largenowplayingcard.go +++ b/ui/widgets/largenowplayingcard.go @@ -4,6 +4,7 @@ import ( "image" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" @@ -46,6 +47,7 @@ func NewLargeNowPlayingCard() *LargeNowPlayingCard { n.rating.StarSize = theme.IconInlineSize() + theme.InnerPadding()/2 n.rating.OnRatingChanged = n.onSetRating n.favorite.OnTapped = n.onToggleFavorite + n.cover.ScaleMode = canvas.ImageScaleFastest // set up the layout n.Content = n.cover n.Caption = container.New(&layouts.VboxCustomPadding{ExtraPad: -13}, diff --git a/ui/widgets/nowplayingcard.go b/ui/widgets/nowplayingcard.go index 13f882e..0ab4368 100644 --- a/ui/widgets/nowplayingcard.go +++ b/ui/widgets/nowplayingcard.go @@ -52,6 +52,7 @@ func NewNowPlayingCard() *NowPlayingCard { n.trackName.TextStyle.Bold = true n.cover.SetMinSize(fyne.NewSize(85, 85)) n.cover.FillMode = canvas.ImageFillContain + n.cover.ScaleMode = canvas.ImageScaleFastest n.cover.Hidden = true n.albumName.OnTapped = n.onAlbumNameTapped n.artistName.OnTapped = n.onArtistNameTapped From aa58a5080406545d9a38fe2ccf73dfe2101f5561 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 3 Mar 2024 11:00:16 -0800 Subject: [PATCH 20/28] add status line to fullscreen page --- ui/browsing/fullscreenpage.go | 123 ++++++++++++++++++++++++++++++---- 1 file changed, 109 insertions(+), 14 deletions(-) diff --git a/ui/browsing/fullscreenpage.go b/ui/browsing/fullscreenpage.go index 65b6ccf..d0b53b6 100644 --- a/ui/browsing/fullscreenpage.go +++ b/ui/browsing/fullscreenpage.go @@ -2,20 +2,26 @@ package browsing import ( "context" + "fmt" "image" "log" + "strings" "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" + "github.com/dweymouth/supersonic/backend/player" + "github.com/dweymouth/supersonic/backend/player/mpv" "github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/layouts" + myTheme "github.com/dweymouth/supersonic/ui/theme" "github.com/dweymouth/supersonic/ui/util" "github.com/dweymouth/supersonic/ui/widgets" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) @@ -28,6 +34,8 @@ type FullscreenPage struct { queueList *widgets.PlayQueueList imageLoadCancel context.CancelFunc card *widgets.LargeNowPlayingCard + statusLabel *widget.Label + totalTime float64 nowPlayingID string albumID string container *fyne.Container @@ -53,6 +61,10 @@ func NewFullscreenPage( }} a.ExtendBaseWidget(a) + pm.OnPaused(a.formatStatusLine) + pm.OnPlaying(a.formatStatusLine) + pm.OnStopped(a.formatStatusLine) + a.card = widgets.NewLargeNowPlayingCard() a.card.DisableRating = !canRate a.card.OnAlbumNameTapped = func() { @@ -75,26 +87,37 @@ func NewFullscreenPage( a.queueList.OnShowArtistPage = func(artistID string) { a.contr.NavigateTo(controller.ArtistRoute(artistID)) } - paddedLayout := &layouts.PercentPadLayout{ - LeftRightObjectPercent: .8, - TopBottomObjectPercent: .8, - } - a.container = container.NewGridWithColumns(2, - container.New(paddedLayout, a.card), - container.New(paddedLayout, - util.AddHeaderBackground( - container.NewAppTabs( - container.NewTabItem("Play Queue", - container.NewBorder(layout.NewSpacer(), nil, nil, nil, a.queueList)), - container.NewTabItem("Lyrics", layout.NewSpacer()), - ))), - ) + a.statusLabel = widget.NewLabel("Stopped") a.Reload() return a } func (a *FullscreenPage) CreateRenderer() fyne.WidgetRenderer { + if a.container == nil { + paddedLayout := &layouts.PercentPadLayout{ + LeftRightObjectPercent: .8, + TopBottomObjectPercent: .8, + } + mainContent := container.NewGridWithColumns(2, + container.New(paddedLayout, a.card), + container.New(paddedLayout, + util.AddHeaderBackground( + container.NewAppTabs( + container.NewTabItem("Play Queue", + container.NewBorder(layout.NewSpacer(), nil, nil, nil, a.queueList)), + container.NewTabItem("Lyrics", layout.NewSpacer()), + )))) + a.container = container.NewStack( + mainContent, + container.NewVBox( + layout.NewSpacer(), + container.NewBorder(nil, nil, util.NewHSpace(1), util.NewHSpace(1), + myTheme.NewThemedRectangle(theme.ColorNameInputBorder)), + a.statusLabel, + ), + ) + } return widget.NewSimpleRenderer(a.container) } @@ -142,6 +165,11 @@ func (a *FullscreenPage) OnPlayQueueChange() { func (a *FullscreenPage) Reload() { a.queue = a.pm.GetPlayQueue() a.queueList.SetTracks(a.queue) + a.totalTime = 0.0 + for _, tr := range a.queue { + a.totalTime += float64(tr.Duration) + } + a.formatStatusLine() } func (s *fullscreenPageState) Restore() Page { @@ -151,3 +179,70 @@ func (s *fullscreenPageState) Restore() Page { } return NewFullscreenPage(s.contr, s.pool, s.im, s.pm, s.canRate) } + +var _ CanShowPlayTime = (*FullscreenPage)(nil) + +func (a *FullscreenPage) OnPlayTimeUpdate(_, _ float64) { + a.formatStatusLine() +} + +func (a *FullscreenPage) formatStatusLine() { + curPlayer := a.pm.CurrentPlayer() + playerStats := curPlayer.GetStatus() + lastStatus := a.statusLabel.Text + state := "Stopped" + switch playerStats.State { + case player.Paused: + state = "Paused" + case player.Playing: + state = "Playing" + } + + dur := 0.0 + if np := a.pm.NowPlaying(); np != nil { + dur = float64(np.Duration) + } + statusSuffix := "" + trackNum := 0 + if state != "Stopped" { + trackNum = a.pm.NowPlayingIndex() + 1 + statusSuffix = fmt.Sprintf(" %s/%s", + util.SecondsToTimeString(playerStats.TimePos), + util.SecondsToTimeString(dur)) + } + status := fmt.Sprintf("%s (%d/%d)%s", state, trackNum, + len(a.queue), statusSuffix) + + mediaInfo := "" + if state != "Stopped" { + mediaInfo = a.formatMediaInfoStr(curPlayer) + } + if mediaInfo != "" { + mediaInfo = " · " + mediaInfo + } + + a.statusLabel.Text = fmt.Sprintf("%s%s | Total time: %s", status, mediaInfo, util.SecondsToTimeString(a.totalTime)) + if lastStatus != a.statusLabel.Text { + a.statusLabel.Refresh() + } +} + +func (a *FullscreenPage) formatMediaInfoStr(player player.BasePlayer) string { + mpv, ok := player.(*mpv.Player) + if !ok { + return "" + } + audioInfo, err := mpv.GetMediaInfo() + if err != nil { + log.Printf("error getting playback status: %s", err.Error()) + return "" + } + codec := audioInfo.Codec + if len(codec) <= 4 && !strings.EqualFold(codec, "opus") { + codec = strings.ToUpper(codec) // FLAC, MP3, AAC, etc + } + + // Note: bit depth intentionally omitted since MPV reports the decoded bit depth + // i.e. 24 bit files get reported as 32 bit. Also b/c bit depth isn't meaningful for lossy. + return fmt.Sprintf("%s %g kHz, %d kbps", codec, float64(audioInfo.Samplerate)/1000, audioInfo.Bitrate/1000) +} From d5aefad7c3844fe82311a3cabd51836943599cbd Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 3 Mar 2024 11:33:44 -0800 Subject: [PATCH 21/28] add context menu to PlayQueueList --- ui/util/tracklistutil.go | 18 ++++++++ ui/widgets/playqueuelist.go | 90 +++++++++++++++++++++++++++++++++++-- ui/widgets/tracklist.go | 18 +++----- 3 files changed, 110 insertions(+), 16 deletions(-) diff --git a/ui/util/tracklistutil.go b/ui/util/tracklistutil.go index 675c6e9..f94049c 100644 --- a/ui/util/tracklistutil.go +++ b/ui/util/tracklistutil.go @@ -16,6 +16,24 @@ func ToTrackListModels(trs []*mediaprovider.Track) []*TrackListModel { }) } +func SelectedTrackModels(tracks []*TrackListModel) []*TrackListModel { + return sharedutil.FilterSlice(tracks, func(tm *TrackListModel) bool { + return tm.Selected + }) +} + +func SelectedTracks(tracks []*TrackListModel) []*mediaprovider.Track { + return sharedutil.MapSlice(SelectedTrackModels(tracks), func(tm *TrackListModel) *mediaprovider.Track { + return tm.Track + }) +} + +func SelectedTrackIDs(tracks []*TrackListModel) []string { + return sharedutil.MapSlice(SelectedTrackModels(tracks), func(tm *TrackListModel) string { + return tm.Track.ID + }) +} + func SelectTrack(tracks []*TrackListModel, idx int) { if tracks[idx].Selected { return diff --git a/ui/widgets/playqueuelist.go b/ui/widgets/playqueuelist.go index 81ac4cc..84989a6 100644 --- a/ui/widgets/playqueuelist.go +++ b/ui/widgets/playqueuelist.go @@ -14,6 +14,7 @@ import ( "fyne.io/fyne/v2/widget" "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" + "github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/os" myTheme "github.com/dweymouth/supersonic/ui/theme" @@ -25,10 +26,21 @@ const thumbnailSize = 52 type PlayQueueList struct { widget.BaseWidget - list *FocusList + DisableRating bool - OnShowArtistPage func(artistID string) - OnPlayTrackAt func(idx int) + // user action callbacks + OnAddToPlaylist func(trackIDs []string) + OnSetFavorite func(trackIDs []string, fav bool) + OnSetRating func(trackIDs []string, rating int) + OnRemoveFromQueue func(trackIDs []string) + OnDownload func(tracks []*mediaprovider.Track, downloadName string) + OnShowArtistPage func(artistID string) + OnPlayTrackAt func(idx int) + OnReorderTracks func(trackIDs []string, op sharedutil.TrackReorderOp) + + list *FocusList + menu *widget.PopUpMenu + ratingSubmenu *fyne.MenuItem nowPlayingID string colLayout *layouts.ColumnsLayout @@ -159,7 +171,79 @@ func (p *PlayQueueList) selectRange(idx int) { } func (p *PlayQueueList) onShowContextMenu(e *fyne.PointEvent, trackIdx int) { + p.selectTrack(trackIdx) + p.list.Refresh() + if p.menu == nil { + playlist := fyne.NewMenuItem("Add to playlist...", func() { + if p.OnAddToPlaylist != nil { + p.OnAddToPlaylist(p.selectedTrackIDs()) + } + }) + playlist.Icon = myTheme.PlaylistIcon + download := fyne.NewMenuItem("Download...", func() { + if p.OnDownload != nil { + p.OnDownload(p.selectedTracks(), "Selected tracks") + } + }) + download.Icon = theme.DownloadIcon() + favorite := fyne.NewMenuItem("Set favorite", func() { + if p.OnSetFavorite != nil { + p.OnSetFavorite(p.selectedTrackIDs(), true) + } + }) + favorite.Icon = myTheme.FavoriteIcon + unfavorite := fyne.NewMenuItem("Unset favorite", func() { + if p.OnSetFavorite != nil { + p.OnSetFavorite(p.selectedTrackIDs(), false) + } + }) + unfavorite.Icon = myTheme.NotFavoriteIcon + p.ratingSubmenu = util.NewRatingSubmenu(func(rating int) { + if p.OnSetRating != nil { + p.OnSetRating(p.selectedTrackIDs(), rating) + } + }) + remove := fyne.NewMenuItem("Remove from queue", func() { + if p.OnRemoveFromQueue != nil { + p.OnRemoveFromQueue(p.selectedTrackIDs()) + } + }) + remove.Icon = theme.ContentRemoveIcon() + reorder := util.NewReorderTracksSubmenu(func(tro sharedutil.TrackReorderOp) { + if p.OnReorderTracks != nil { + p.OnReorderTracks(p.selectedTrackIDs(), tro) + } + }) + p.menu = widget.NewPopUpMenu( + fyne.NewMenu("", + playlist, + download, + fyne.NewMenuItemSeparator(), + favorite, + unfavorite, + p.ratingSubmenu, + fyne.NewMenuItemSeparator(), + reorder, + remove, + ), + fyne.CurrentApp().Driver().CanvasForObject(p), + ) + } + p.ratingSubmenu.Disabled = p.DisableRating + p.menu.ShowAtPosition(e.AbsolutePosition) +} + +func (t *PlayQueueList) selectedTracks() []*mediaprovider.Track { + t.tracksMutex.RLock() + defer t.tracksMutex.RUnlock() + return util.SelectedTracks(t.tracks) +} + +func (t *PlayQueueList) selectedTrackIDs() []string { + t.tracksMutex.RLock() + defer t.tracksMutex.RUnlock() + return util.SelectedTrackIDs(t.tracks) } func (p *PlayQueueList) CreateRenderer() fyne.WidgetRenderer { diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 4b17503..2a78968 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -629,24 +629,16 @@ func (t *Tracklist) onDownload(tracks []*mediaprovider.Track, downloadName strin } } -func (t *Tracklist) selectedTrackModels() []*util.TrackListModel { +func (t *Tracklist) selectedTracks() []*mediaprovider.Track { t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() - return sharedutil.FilterSlice(t.tracks, func(tm *util.TrackListModel) bool { - return tm.Selected - }) -} - -func (t *Tracklist) selectedTracks() []*mediaprovider.Track { - return sharedutil.MapSlice(t.selectedTrackModels(), func(tm *util.TrackListModel) *mediaprovider.Track { - return tm.Track - }) + return util.SelectedTracks(t.tracks) } func (t *Tracklist) SelectedTrackIDs() []string { - return sharedutil.MapSlice(t.selectedTrackModels(), func(tm *util.TrackListModel) string { - return tm.Track.ID - }) + t.tracksMutex.RLock() + defer t.tracksMutex.RUnlock() + return util.SelectedTrackIDs(t.tracks) } func (t *Tracklist) lenTracks() int { From a86533b42436374271115bce8e31249b16d50d97 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 3 Mar 2024 11:47:57 -0800 Subject: [PATCH 22/28] connect up PlayQueueList actions and remove old NowPlayingPage --- ui/bottompanel.go | 2 +- ui/browsing/fullscreenpage.go | 68 ++++++--- ui/browsing/nowplayingpage.go | 257 ---------------------------------- ui/browsing/router.go | 4 +- ui/controller/routes.go | 4 - ui/util/widgetpool.go | 2 +- ui/widgets/playqueuelist.go | 7 + 7 files changed, 57 insertions(+), 287 deletions(-) delete mode 100644 ui/browsing/nowplayingpage.go diff --git a/ui/bottompanel.go b/ui/bottompanel.go index a4210f3..5332624 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -53,7 +53,7 @@ func NewBottomPanel(pm *backend.PlaybackManager, contr *controller.Controller) * bp.NowPlaying = widgets.NewNowPlayingCard() bp.NowPlaying.OnCoverTapped = func() { - contr.NavigateTo(controller.FullscreenRoute()) + contr.NavigateTo(controller.NowPlayingRoute("")) } bp.NowPlaying.OnSetFavorite = func(fav bool) { contr.SetTrackFavorites([]string{pm.NowPlaying().ID}, fav) diff --git a/ui/browsing/fullscreenpage.go b/ui/browsing/fullscreenpage.go index d0b53b6..5dd938e 100644 --- a/ui/browsing/fullscreenpage.go +++ b/ui/browsing/fullscreenpage.go @@ -25,10 +25,10 @@ import ( "fyne.io/fyne/v2/widget" ) -type FullscreenPage struct { +type NowPlayingPage struct { widget.BaseWidget - fullscreenPageState + nowPlayingPageState queue []*mediaprovider.Track queueList *widgets.PlayQueueList @@ -41,7 +41,7 @@ type FullscreenPage struct { container *fyne.Container } -type fullscreenPageState struct { +type nowPlayingPageState struct { contr *controller.Controller pool *util.WidgetPool pm *backend.PlaybackManager @@ -49,14 +49,14 @@ type fullscreenPageState struct { canRate bool } -func NewFullscreenPage( +func NewNowPlayingPage( contr *controller.Controller, pool *util.WidgetPool, im *backend.ImageManager, pm *backend.PlaybackManager, canRate bool, -) *FullscreenPage { - a := &FullscreenPage{fullscreenPageState: fullscreenPageState{ +) *NowPlayingPage { + a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{ contr: contr, pool: pool, im: im, pm: pm, canRate: canRate, }} a.ExtendBaseWidget(a) @@ -81,19 +81,29 @@ func NewFullscreenPage( } a.queueList = widgets.NewPlayQueueList(a.im) + a.queueList.OnReorderTracks = a.doSetNewTrackOrder + a.queueList.OnDownload = contr.ShowDownloadDialog + a.queueList.OnSetRating = contr.SetTrackRatings + a.queueList.OnSetFavorite = contr.SetTrackFavorites + a.queueList.OnAddToPlaylist = contr.DoAddTracksToPlaylistWorkflow a.queueList.OnPlayTrackAt = func(tracknum int) { _ = a.pm.PlayTrackAt(tracknum) } a.queueList.OnShowArtistPage = func(artistID string) { a.contr.NavigateTo(controller.ArtistRoute(artistID)) } + a.queueList.OnRemoveFromQueue = func(trackIDs []string) { + a.queueList.UnselectAll() + a.pm.RemoveTracksFromQueue(trackIDs) + } + a.statusLabel = widget.NewLabel("Stopped") a.Reload() return a } -func (a *FullscreenPage) CreateRenderer() fyne.WidgetRenderer { +func (a *NowPlayingPage) CreateRenderer() fyne.WidgetRenderer { if a.container == nil { paddedLayout := &layouts.PercentPadLayout{ LeftRightObjectPercent: .8, @@ -121,22 +131,22 @@ func (a *FullscreenPage) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(a.container) } -func (a *FullscreenPage) Save() SavedPage { +func (a *NowPlayingPage) Save() SavedPage { if a.imageLoadCancel != nil { a.imageLoadCancel() } - nps := a.fullscreenPageState - a.pool.Release(util.WidgetTypeFullscreenPage, a) + nps := a.nowPlayingPageState + a.pool.Release(util.WidgetTypeNowPlayingPage, a) return &nps } -func (a *FullscreenPage) Route() controller.Route { +func (a *NowPlayingPage) Route() controller.Route { return controller.NowPlayingRoute("") } -var _ CanShowNowPlaying = (*FullscreenPage)(nil) +var _ CanShowNowPlaying = (*NowPlayingPage)(nil) -func (a *FullscreenPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Track) { +func (a *NowPlayingPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Track) { if a.imageLoadCancel != nil { a.imageLoadCancel() } @@ -158,11 +168,11 @@ func (a *FullscreenPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Tr }) } -func (a *FullscreenPage) OnPlayQueueChange() { +func (a *NowPlayingPage) OnPlayQueueChange() { a.Reload() } -func (a *FullscreenPage) Reload() { +func (a *NowPlayingPage) Reload() { a.queue = a.pm.GetPlayQueue() a.queueList.SetTracks(a.queue) a.totalTime = 0.0 @@ -172,21 +182,35 @@ func (a *FullscreenPage) Reload() { a.formatStatusLine() } -func (s *fullscreenPageState) Restore() Page { - if page := s.pool.Obtain(util.WidgetTypeFullscreenPage).(*FullscreenPage); page != nil { +func (s *nowPlayingPageState) Restore() Page { + if page := s.pool.Obtain(util.WidgetTypeNowPlayingPage).(*NowPlayingPage); page != nil { page.Reload() return page } - return NewFullscreenPage(s.contr, s.pool, s.im, s.pm, s.canRate) + return NewNowPlayingPage(s.contr, s.pool, s.im, s.pm, s.canRate) } -var _ CanShowPlayTime = (*FullscreenPage)(nil) +var _ CanShowPlayTime = (*NowPlayingPage)(nil) -func (a *FullscreenPage) OnPlayTimeUpdate(_, _ float64) { +func (a *NowPlayingPage) OnPlayTimeUpdate(_, _ float64) { a.formatStatusLine() } -func (a *FullscreenPage) formatStatusLine() { +func (a *NowPlayingPage) doSetNewTrackOrder(trackIDs []string, op sharedutil.TrackReorderOp) { + // Since the tracklist view may be sorted in a different order than the + // actual running order, we need to get the IDs of the selected tracks + // from the tracklist and convert them to indices in the *original* run order + idxs := make([]int, 0, len(trackIDs)) + for i, tr := range a.queue { + if sharedutil.SliceContains(trackIDs, tr.ID) { + idxs = append(idxs, i) + } + } + newTracks := sharedutil.ReorderTracks(a.queue, idxs, op) + a.pm.UpdatePlayQueue(newTracks) +} + +func (a *NowPlayingPage) formatStatusLine() { curPlayer := a.pm.CurrentPlayer() playerStats := curPlayer.GetStatus() lastStatus := a.statusLabel.Text @@ -227,7 +251,7 @@ func (a *FullscreenPage) formatStatusLine() { } } -func (a *FullscreenPage) formatMediaInfoStr(player player.BasePlayer) string { +func (a *NowPlayingPage) formatMediaInfoStr(player player.BasePlayer) string { mpv, ok := player.(*mpv.Player) if !ok { return "" diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go deleted file mode 100644 index 552fce0..0000000 --- a/ui/browsing/nowplayingpage.go +++ /dev/null @@ -1,257 +0,0 @@ -package browsing - -import ( - "fmt" - "log" - "strings" - - "github.com/dweymouth/supersonic/backend" - "github.com/dweymouth/supersonic/backend/mediaprovider" - "github.com/dweymouth/supersonic/backend/player" - "github.com/dweymouth/supersonic/backend/player/mpv" - "github.com/dweymouth/supersonic/sharedutil" - "github.com/dweymouth/supersonic/ui/controller" - "github.com/dweymouth/supersonic/ui/layouts" - myTheme "github.com/dweymouth/supersonic/ui/theme" - "github.com/dweymouth/supersonic/ui/util" - "github.com/dweymouth/supersonic/ui/widgets" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/theme" - "fyne.io/fyne/v2/widget" -) - -type NowPlayingPage struct { - widget.BaseWidget - - nowPlayingPageState - - queue []*mediaprovider.Track - totalTime float64 - - title *widget.RichText - tracklist *widgets.Tracklist - statusLabel *widget.Label - nowPlayingID string - container *fyne.Container -} - -type nowPlayingPageState struct { - contr *controller.Controller - pool *util.WidgetPool - conf *backend.NowPlayingPageConfig - pm *backend.PlaybackManager - canRate bool -} - -func NewNowPlayingPage( - highlightedTrackID string, - contr *controller.Controller, - pool *util.WidgetPool, - conf *backend.NowPlayingPageConfig, - pm *backend.PlaybackManager, - canRate bool, -) *NowPlayingPage { - a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{ - contr: contr, pool: pool, conf: conf, pm: pm, canRate: canRate, - }} - a.ExtendBaseWidget(a) - - pm.OnPaused(a.formatStatusLine) - pm.OnPlaying(a.formatStatusLine) - pm.OnStopped(a.formatStatusLine) - - if t := a.pool.Obtain(util.WidgetTypeTracklist); t != nil { - a.tracklist = t.(*widgets.Tracklist) - a.tracklist.Reset() - } else { - a.tracklist = widgets.NewTracklist(nil) - } - a.tracklist.SetVisibleColumns(conf.TracklistColumns) - a.tracklist.OnVisibleColumnsChanged = func(cols []string) { - a.conf.TracklistColumns = cols - } - remove := fyne.NewMenuItem("Remove from queue", a.onRemoveSelectedFromQueue) - remove.Icon = theme.ContentRemoveIcon() - a.tracklist.Options = widgets.TracklistOptions{ - AutoNumber: true, - DisablePlaybackMenu: true, - DisableRating: !canRate, - AuxiliaryMenuItems: []*fyne.MenuItem{ - util.NewReorderTracksSubmenu(a.doSetNewTrackOrder), - remove, - }, - } - contr.ConnectTracklistActions(a.tracklist) - // override the default OnPlayTrackAt handler b/c we don't need to re-load the tracks into the queue - a.tracklist.OnPlayTrackAt = a.onPlayTrackAt - a.title = widget.NewRichTextWithText("Now Playing") - a.title.Segments[0].(*widget.TextSegment).Style.SizeName = widget.RichTextStyleHeading.SizeName - a.statusLabel = widget.NewLabel("Stopped") - statusLabelCtr := container.New(&layouts.VboxCustomPadding{ExtraPad: -5}, - myTheme.NewThemedRectangle(theme.ColorNameInputBorder), - a.statusLabel, - ) - a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15}, - container.NewBorder(a.title, statusLabelCtr, nil, nil, a.tracklist)) - a.load(highlightedTrackID) - return a -} - -func (a *NowPlayingPage) CreateRenderer() fyne.WidgetRenderer { - return widget.NewSimpleRenderer(a.container) -} - -func (a *NowPlayingPage) Save() SavedPage { - a.tracklist.Clear() - a.pool.Release(util.WidgetTypeTracklist, a.tracklist) - nps := a.nowPlayingPageState - return &nps -} - -func (a *NowPlayingPage) Route() controller.Route { - return controller.NowPlayingRoute("") -} - -func (a *NowPlayingPage) Tapped(*fyne.PointEvent) { - a.tracklist.UnselectAll() -} - -func (a *NowPlayingPage) SelectAll() { - a.tracklist.SelectAll() -} - -var _ CanShowNowPlaying = (*NowPlayingPage)(nil) - -func (a *NowPlayingPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Track) { - a.nowPlayingID = sharedutil.TrackIDOrEmptyStr(song) - a.tracklist.SetNowPlaying(a.nowPlayingID) - a.tracklist.IncrementPlayCount(sharedutil.TrackIDOrEmptyStr(lastScrobbledIfAny)) -} - -var _ CanShowPlayTime = (*NowPlayingPage)(nil) - -func (a *NowPlayingPage) OnPlayTimeUpdate(_, _ float64) { - a.formatStatusLine() -} - -func (a *NowPlayingPage) formatStatusLine() { - curPlayer := a.pm.CurrentPlayer() - playerStats := curPlayer.GetStatus() - lastStatus := a.statusLabel.Text - state := "Stopped" - switch playerStats.State { - case player.Paused: - state = "Paused" - case player.Playing: - state = "Playing" - } - - dur := 0.0 - if np := a.pm.NowPlaying(); np != nil { - dur = float64(np.Duration) - } - statusSuffix := "" - trackNum := 0 - if state != "Stopped" { - trackNum = a.pm.NowPlayingIndex() + 1 - statusSuffix = fmt.Sprintf(" %s/%s", - util.SecondsToTimeString(playerStats.TimePos), - util.SecondsToTimeString(dur)) - } - status := fmt.Sprintf("%s (%d/%d)%s", state, trackNum, - len(a.queue), statusSuffix) - - mediaInfo := "" - if state != "Stopped" { - mediaInfo = a.formatMediaInfoStr(curPlayer) - } - if mediaInfo != "" { - mediaInfo = " · " + mediaInfo - } - - a.statusLabel.Text = fmt.Sprintf("%s%s | Total time: %s", status, mediaInfo, util.SecondsToTimeString(a.totalTime)) - if lastStatus != a.statusLabel.Text { - a.statusLabel.Refresh() - } -} - -func (a *NowPlayingPage) formatMediaInfoStr(player player.BasePlayer) string { - mpv, ok := player.(*mpv.Player) - if !ok { - return "" - } - audioInfo, err := mpv.GetMediaInfo() - if err != nil { - log.Printf("error getting playback status: %s", err.Error()) - return "" - } - codec := audioInfo.Codec - if len(codec) <= 4 && !strings.EqualFold(codec, "opus") { - codec = strings.ToUpper(codec) // FLAC, MP3, AAC, etc - } - - // Note: bit depth intentionally omitted since MPV reports the decoded bit depth - // i.e. 24 bit files get reported as 32 bit. Also b/c bit depth isn't meaningful for lossy. - return fmt.Sprintf("%s %g kHz, %d kbps", codec, float64(audioInfo.Samplerate)/1000, audioInfo.Bitrate/1000) -} - -func (a *NowPlayingPage) Reload() { - a.load("") -} - -var _ Scrollable = (*NowPlayingPage)(nil) - -func (a *NowPlayingPage) Scroll(scrollAmt float32) { - a.tracklist.Scroll(scrollAmt) -} - -func (a *NowPlayingPage) onPlayTrackAt(tracknum int) { - _ = a.pm.PlayTrackAt(tracknum) -} - -func (a *NowPlayingPage) onRemoveSelectedFromQueue() { - a.pm.RemoveTracksFromQueue(a.tracklist.SelectedTrackIDs()) - a.tracklist.UnselectAll() - a.Reload() -} - -func (a *NowPlayingPage) doSetNewTrackOrder(op sharedutil.TrackReorderOp) { - // Since the tracklist view may be sorted in a different order than the - // actual running order, we need to get the IDs of the selected tracks - // from the tracklist and convert them to indices in the *original* run order - ids := a.tracklist.SelectedTrackIDs() - idxs := make([]int, 0, len(ids)) - for i, tr := range a.queue { - if sharedutil.SliceContains(ids, tr.ID) { - idxs = append(idxs, i) - } - } - newTracks := sharedutil.ReorderTracks(a.queue, idxs, op) - a.pm.UpdatePlayQueue(newTracks) - - // force-switch back to unsorted view to show new track order - a.tracklist.SetSorting(widgets.TracklistSort{}) - a.tracklist.SetTracks(newTracks) - a.tracklist.UnselectAll() -} - -// does not make calls to server - can safely be run in UI callbacks -func (a *NowPlayingPage) load(highlightedTrackID string) { - a.queue = a.pm.GetPlayQueue() - a.tracklist.SetTracks(a.queue) - a.tracklist.SetNowPlaying(a.nowPlayingID) - if highlightedTrackID != "" { - a.tracklist.SelectAndScrollToTrack(highlightedTrackID) - } - a.totalTime = 0.0 - for _, tr := range a.queue { - a.totalTime += float64(tr.Duration) - } - a.formatStatusLine() -} - -func (s *nowPlayingPageState) Restore() Page { - return NewNowPlayingPage("", s.contr, s.pool, s.conf, s.pm, s.canRate) -} diff --git a/ui/browsing/router.go b/ui/browsing/router.go index f515a16..900ce7e 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -43,13 +43,13 @@ func (r Router) CreatePage(rte controller.Route) Page { case controller.Favorites: return NewFavoritesPage(&r.App.Config.FavoritesPage, r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager) case controller.Fullscreen: - return NewFullscreenPage(r.Controller, r.widgetPool, r.App.ImageManager, r.App.PlaybackManager, canRate) + return NewNowPlayingPage(r.Controller, r.widgetPool, r.App.ImageManager, r.App.PlaybackManager, canRate) case controller.Genre: return NewGenrePage(rte.Arg, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Genres: return NewGenresPage(r.Controller, r.App.ServerManager.Server) case controller.NowPlaying: - return NewNowPlayingPage(rte.Arg, r.Controller, r.widgetPool, &r.App.Config.NowPlayingPage, r.App.PlaybackManager, canRate) + return NewNowPlayingPage(r.Controller, r.widgetPool, r.App.ImageManager, r.App.PlaybackManager, canRate) case controller.Playlist: return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.widgetPool, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager) case controller.Playlists: diff --git a/ui/controller/routes.go b/ui/controller/routes.go index 172175a..a351b8f 100644 --- a/ui/controller/routes.go +++ b/ui/controller/routes.go @@ -39,10 +39,6 @@ func FavoritesRoute() Route { return Route{Page: Favorites} } -func FullscreenRoute() Route { - return Route{Page: Fullscreen} -} - func GenreRoute(genre string) Route { return Route{Page: Genre, Arg: genre} } diff --git a/ui/util/widgetpool.go b/ui/util/widgetpool.go index 0db2ef0..b8c574e 100644 --- a/ui/util/widgetpool.go +++ b/ui/util/widgetpool.go @@ -15,7 +15,7 @@ const ( WidgetTypeGridView WidgetTypePlaylistPageHeader WidgetTypeTracklist - WidgetTypeFullscreenPage + WidgetTypeNowPlayingPage // keep at bottom numWidgetTypes diff --git a/ui/widgets/playqueuelist.go b/ui/widgets/playqueuelist.go index 84989a6..c3bb707 100644 --- a/ui/widgets/playqueuelist.go +++ b/ui/widgets/playqueuelist.go @@ -118,6 +118,13 @@ func (p *PlayQueueList) SetNowPlaying(trackID string) { } } +func (p *PlayQueueList) UnselectAll() { + p.tracksMutex.RLock() + util.UnselectAllTracks(p.tracks) + p.tracksMutex.RUnlock() + p.Refresh() +} + func (p *PlayQueueList) lenTracks() int { p.tracksMutex.RLock() defer p.tracksMutex.RUnlock() From 151dbb9edf9d33fb016a853f634f96a254944002 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 3 Mar 2024 11:48:27 -0800 Subject: [PATCH 23/28] rename file --- ui/browsing/{fullscreenpage.go => nowplayingpage.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ui/browsing/{fullscreenpage.go => nowplayingpage.go} (100%) diff --git a/ui/browsing/fullscreenpage.go b/ui/browsing/nowplayingpage.go similarity index 100% rename from ui/browsing/fullscreenpage.go rename to ui/browsing/nowplayingpage.go From 67c5edbbdef258427032f0294e452fae23dd7f3f Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 3 Mar 2024 11:50:07 -0800 Subject: [PATCH 24/28] rm irrelevant comment --- ui/browsing/nowplayingpage.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index 5dd938e..0c1e737 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -197,9 +197,6 @@ func (a *NowPlayingPage) OnPlayTimeUpdate(_, _ float64) { } func (a *NowPlayingPage) doSetNewTrackOrder(trackIDs []string, op sharedutil.TrackReorderOp) { - // Since the tracklist view may be sorted in a different order than the - // actual running order, we need to get the IDs of the selected tracks - // from the tracklist and convert them to indices in the *original* run order idxs := make([]int, 0, len(trackIDs)) for i, tr := range a.queue { if sharedutil.SliceContains(trackIDs, tr.ID) { From eaef666b0ccac9d3958d5517cdc9754b85b04199 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 3 Mar 2024 13:48:45 -0800 Subject: [PATCH 25/28] make sure to update displayed rating/favorite when changing from list --- ui/browsing/nowplayingpage.go | 14 ++++++++++++-- ui/widgets/largenowplayingcard.go | 10 ++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index 0c1e737..ecabf05 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -83,8 +83,6 @@ func NewNowPlayingPage( a.queueList = widgets.NewPlayQueueList(a.im) a.queueList.OnReorderTracks = a.doSetNewTrackOrder a.queueList.OnDownload = contr.ShowDownloadDialog - a.queueList.OnSetRating = contr.SetTrackRatings - a.queueList.OnSetFavorite = contr.SetTrackFavorites a.queueList.OnAddToPlaylist = contr.DoAddTracksToPlaylistWorkflow a.queueList.OnPlayTrackAt = func(tracknum int) { _ = a.pm.PlayTrackAt(tracknum) @@ -96,6 +94,18 @@ func NewNowPlayingPage( a.queueList.UnselectAll() a.pm.RemoveTracksFromQueue(trackIDs) } + a.queueList.OnSetRating = func(trackIDs []string, rating int) { + contr.SetTrackRatings(trackIDs, rating) + if sharedutil.SliceContains(trackIDs, a.nowPlayingID) { + a.card.SetDisplayedRating(rating) + } + } + a.queueList.OnSetFavorite = func(trackIDs []string, fav bool) { + contr.SetTrackFavorites(trackIDs, fav) + if sharedutil.SliceContains(trackIDs, a.nowPlayingID) { + a.card.SetDisplayedFavorite(fav) + } + } a.statusLabel = widget.NewLabel("Stopped") diff --git a/ui/widgets/largenowplayingcard.go b/ui/widgets/largenowplayingcard.go index fcea659..4278519 100644 --- a/ui/widgets/largenowplayingcard.go +++ b/ui/widgets/largenowplayingcard.go @@ -99,6 +99,16 @@ func (n *LargeNowPlayingCard) onSetRating(rating int) { } } +func (n *LargeNowPlayingCard) SetDisplayedRating(rating int) { + n.rating.Rating = rating + n.rating.Refresh() +} + +func (n *LargeNowPlayingCard) SetDisplayedFavorite(favorite bool) { + n.favorite.Favorite = favorite + n.favorite.Refresh() +} + func (n *LargeNowPlayingCard) Update(track *mediaprovider.Track) { if track != nil { n.trackName.Segments[0].(*widget.TextSegment).Text = track.Name From e9142f9ebf60948cfd97f8b6a4ee1d59520086e2 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 3 Mar 2024 13:51:54 -0800 Subject: [PATCH 26/28] add stub lyrics viewer component --- ui/browsing/nowplayingpage.go | 4 +++- ui/widgets/lyricsviewer.go | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 ui/widgets/lyricsviewer.go diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index ecabf05..ffaf9d7 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -32,6 +32,7 @@ type NowPlayingPage struct { queue []*mediaprovider.Track queueList *widgets.PlayQueueList + lyricsViewer *widgets.LyricsViewer imageLoadCancel context.CancelFunc card *widgets.LargeNowPlayingCard statusLabel *widget.Label @@ -107,6 +108,7 @@ func NewNowPlayingPage( } } + a.lyricsViewer = widgets.NewLyricsViewer() a.statusLabel = widget.NewLabel("Stopped") a.Reload() @@ -126,7 +128,7 @@ func (a *NowPlayingPage) CreateRenderer() fyne.WidgetRenderer { container.NewAppTabs( container.NewTabItem("Play Queue", container.NewBorder(layout.NewSpacer(), nil, nil, nil, a.queueList)), - container.NewTabItem("Lyrics", layout.NewSpacer()), + container.NewTabItem("Lyrics", a.lyricsViewer), )))) a.container = container.NewStack( mainContent, diff --git a/ui/widgets/lyricsviewer.go b/ui/widgets/lyricsviewer.go new file mode 100644 index 0000000..683771f --- /dev/null +++ b/ui/widgets/lyricsviewer.go @@ -0,0 +1,15 @@ +package widgets + +import "fyne.io/fyne/v2/widget" + +type LyricsViewer struct { + widget.Label +} + +func NewLyricsViewer() *LyricsViewer { + l := &LyricsViewer{Label: widget.Label{ + Text: "Lyrics not available", + }} + l.ExtendBaseWidget(l) + return l +} From 44abf3c4348c385fb80ba0f67119c2dd560e160b Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 3 Mar 2024 13:58:15 -0800 Subject: [PATCH 27/28] add select all support to new NowPlaying page --- ui/browsing/nowplayingpage.go | 12 ++++++++++++ ui/widgets/playqueuelist.go | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index ffaf9d7..e14afa3 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -208,6 +208,18 @@ func (a *NowPlayingPage) OnPlayTimeUpdate(_, _ float64) { a.formatStatusLine() } +var _ CanSelectAll = (*NowPlayingPage)(nil) + +func (a *NowPlayingPage) SelectAll() { + a.queueList.SelectAll() +} + +var _ fyne.Tappable = (*NowPlayingPage)(nil) + +func (a *NowPlayingPage) Tapped(*fyne.PointEvent) { + a.queueList.UnselectAll() +} + func (a *NowPlayingPage) doSetNewTrackOrder(trackIDs []string, op sharedutil.TrackReorderOp) { idxs := make([]int, 0, len(trackIDs)) for i, tr := range a.queue { diff --git a/ui/widgets/playqueuelist.go b/ui/widgets/playqueuelist.go index c3bb707..c8855d9 100644 --- a/ui/widgets/playqueuelist.go +++ b/ui/widgets/playqueuelist.go @@ -118,6 +118,13 @@ func (p *PlayQueueList) SetNowPlaying(trackID string) { } } +func (p *PlayQueueList) SelectAll() { + p.tracksMutex.RLock() + util.SelectAllTracks(p.tracks) + p.tracksMutex.RUnlock() + p.list.Refresh() +} + func (p *PlayQueueList) UnselectAll() { p.tracksMutex.RLock() util.UnselectAllTracks(p.tracks) From cd31274a81391f6adb9a6b2a4a7cd9afae677a4f Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 3 Mar 2024 14:06:19 -0800 Subject: [PATCH 28/28] remove defunct NowPlayingPageConfig --- backend/config.go | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/backend/config.go b/backend/config.go index d45acff..8dfc16e 100644 --- a/backend/config.go +++ b/backend/config.go @@ -69,10 +69,6 @@ type FavoritesPageConfig struct { TracklistColumns []string } -type NowPlayingPageConfig struct { - TracklistColumns []string -} - type PlaylistPageConfig struct { TracklistColumns []string } @@ -117,21 +113,20 @@ type TranscodingConfig struct { } type Config struct { - Application AppConfig - Servers []*ServerConfig - AlbumPage AlbumPageConfig - AlbumsPage AlbumsPageConfig - ArtistPage ArtistPageConfig - FavoritesPage FavoritesPageConfig - NowPlayingPage NowPlayingPageConfig - PlaylistPage PlaylistPageConfig - PlaylistsPage PlaylistsPageConfig - TracksPage TracksPageConfig - LocalPlayback LocalPlaybackConfig - Scrobbling ScrobbleConfig - ReplayGain ReplayGainConfig - Transcoding TranscodingConfig - Theme ThemeConfig + Application AppConfig + Servers []*ServerConfig + AlbumPage AlbumPageConfig + AlbumsPage AlbumsPageConfig + ArtistPage ArtistPageConfig + FavoritesPage FavoritesPageConfig + PlaylistPage PlaylistPageConfig + PlaylistsPage PlaylistsPageConfig + TracksPage TracksPageConfig + LocalPlayback LocalPlaybackConfig + Scrobbling ScrobbleConfig + ReplayGain ReplayGainConfig + Transcoding TranscodingConfig + Theme ThemeConfig } var SupportedStartupPages = []string{"Albums", "Favorites", "Playlists"} @@ -167,9 +162,6 @@ func DefaultConfig(appVersionTag string) *Config { TracklistColumns: []string{"Artist", "Album", "Time", "Plays"}, InitialView: "Albums", }, - NowPlayingPage: NowPlayingPageConfig{ - TracklistColumns: []string{"Artist", "Album", "Time", "Plays"}, - }, PlaylistPage: PlaylistPageConfig{ TracklistColumns: []string{"Artist", "Album", "Time", "Plays"}, },