From 22e9287608f8a3fa766cf35220129021254d71b3 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 10 Feb 2023 18:02:08 -0800 Subject: [PATCH 01/12] refactor: get rid of browsingpane as play album mediator --- ui/browsing/albumpage.go | 10 +--------- ui/browsing/albumspage.go | 38 ++++++++++++++++++------------------- ui/browsing/browsingpane.go | 9 --------- ui/browsing/router.go | 2 +- 4 files changed, 20 insertions(+), 39 deletions(-) diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 7e90870..b5957c4 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -26,8 +26,6 @@ type AlbumPage struct { tracklist *widgets.Tracklist nowPlayingID string container *fyne.Container - - OnPlayAlbum func(string, int) } type albumPageState struct { @@ -84,10 +82,6 @@ func (a *AlbumPage) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(a.container) } -func (a *AlbumPage) SetPlayAlbumCallback(cb func(string, int)) { - a.OnPlayAlbum = cb -} - func (a *AlbumPage) Save() SavedPage { s := a.albumPageState return &s @@ -115,9 +109,7 @@ func (a *AlbumPage) Tapped(*fyne.PointEvent) { } func (a *AlbumPage) onPlayTrackAt(tracknum int) { - if a.OnPlayAlbum != nil { - a.OnPlayAlbum(a.albumID, tracknum) - } + a.pm.PlayAlbum(a.albumID, tracknum) } func (a *AlbumPage) loadAsync() { diff --git a/ui/browsing/albumspage.go b/ui/browsing/albumspage.go index 3368d00..c5b83c8 100644 --- a/ui/browsing/albumspage.go +++ b/ui/browsing/albumspage.go @@ -16,18 +16,18 @@ var _ fyne.Widget = (*AlbumsPage)(nil) type AlbumsPage struct { widget.BaseWidget - title string - im *backend.ImageManager - lm *backend.LibraryManager - nav func(Route) - grid *widgets.AlbumGrid - searchGrid *widgets.AlbumGrid - searcher *widgets.Searcher - searchText string - titleDisp *widget.RichText - sortOrder *selectWidget - container *fyne.Container - OnPlayAlbum func(string, int) + title string + pm *backend.PlaybackManager + im *backend.ImageManager + lm *backend.LibraryManager + nav func(Route) + grid *widgets.AlbumGrid + searchGrid *widgets.AlbumGrid + searcher *widgets.Searcher + searchText string + titleDisp *widget.RichText + sortOrder *selectWidget + container *fyne.Container } type selectWidget struct { @@ -51,9 +51,10 @@ func (s *selectWidget) MinSize() fyne.Size { return fyne.NewSize(170, s.height) } -func NewAlbumsPage(title string, sortOrder string, lm *backend.LibraryManager, im *backend.ImageManager, nav func(Route)) *AlbumsPage { +func NewAlbumsPage(title string, sortOrder string, pm *backend.PlaybackManager, lm *backend.LibraryManager, im *backend.ImageManager, nav func(Route)) *AlbumsPage { a := &AlbumsPage{ title: title, + pm: pm, lm: lm, im: im, nav: nav, @@ -98,6 +99,7 @@ func (a *AlbumsPage) createContainer(searchgrid bool) { func restoreAlbumsPage(saved *savedAlbumsPage) *AlbumsPage { a := &AlbumsPage{ title: saved.title, + pm: saved.pm, lm: saved.lm, im: saved.im, nav: saved.nav, @@ -146,10 +148,6 @@ func (a *AlbumsPage) SearchWidget() fyne.Focusable { return a.searcher.Entry } -func (a *AlbumsPage) SetPlayAlbumCallback(cb func(string, int)) { - a.OnPlayAlbum = cb -} - func (a *AlbumsPage) Reload() { if a.searchText != "" { a.doSearch(a.searchText) @@ -162,6 +160,7 @@ func (a *AlbumsPage) Reload() { func (a *AlbumsPage) Save() SavedPage { sa := &savedAlbumsPage{ title: a.title, + pm: a.pm, lm: a.lm, im: a.im, nav: a.nav, @@ -189,9 +188,7 @@ func (a *AlbumsPage) doSearch(query string) { } func (a *AlbumsPage) onPlayAlbum(albumID string) { - if a.OnPlayAlbum != nil { - a.OnPlayAlbum(albumID, 0) - } + a.pm.PlayAlbum(albumID, 0) } func (a *AlbumsPage) onShowArtistPage(artistID string) { @@ -218,6 +215,7 @@ func (a *AlbumsPage) CreateRenderer() fyne.WidgetRenderer { type savedAlbumsPage struct { title string searchText string + pm *backend.PlaybackManager lm *backend.LibraryManager im *backend.ImageManager nav func(Route) diff --git a/ui/browsing/browsingpane.go b/ui/browsing/browsingpane.go index 4efed1c..278fa25 100644 --- a/ui/browsing/browsingpane.go +++ b/ui/browsing/browsingpane.go @@ -31,10 +31,6 @@ type Searchable interface { SearchWidget() fyne.Focusable } -type CanPlayAlbum interface { - SetPlayAlbumCallback(func(albumID string, startingTrack int)) -} - type CanShowNowPlaying interface { OnSongChange(song *subsonic.Child) } @@ -98,11 +94,6 @@ func (b *BrowsingPane) doSetPage(p Page) bool { return false } b.curPage = p - if pa, ok := p.(CanPlayAlbum); ok { - pa.SetPlayAlbumCallback(func(albumID string, firstTrack int) { - _ = b.app.PlaybackManager.PlayAlbum(albumID, firstTrack) - }) - } if np, ok := p.(CanShowNowPlaying); ok { np.OnSongChange(b.app.PlaybackManager.NowPlaying()) } diff --git a/ui/browsing/router.go b/ui/browsing/router.go index 32ac87b..e95b453 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -87,7 +87,7 @@ func (r Router) CreatePage(rte Route) Page { case Album: return NewAlbumPage(rte.Arg, r.App.ServerManager, r.App.PlaybackManager, r.App.LibraryManager, r.App.ImageManager, r.Controller, r.OpenRoute) case Albums: - return NewAlbumsPage("Albums", rte.Arg, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute) + return NewAlbumsPage("Albums", rte.Arg, r.App.PlaybackManager, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute) case Artist: return NewArtistPage(rte.Arg, r.App.ServerManager, r.App.ImageManager, r.Controller, r.OpenRoute) case Artists: From 6b8acc6fa193fc38ab0e68f6616bc994aafa23a0 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 10 Feb 2023 18:05:33 -0800 Subject: [PATCH 02/12] continue refactor for favorites page --- ui/browsing/favoritespage.go | 17 +++++++---------- ui/browsing/router.go | 2 +- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index 82e2b3f..16f6832 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -16,6 +16,7 @@ import ( type FavoritesPage struct { widget.BaseWidget + pm *backend.PlaybackManager im *backend.ImageManager sm *backend.ServerManager lm *backend.LibraryManager @@ -26,12 +27,11 @@ type FavoritesPage struct { searchText string titleDisp *widget.RichText container *fyne.Container - - OnPlayAlbum func(string, int) } -func NewFavoritesPage(sm *backend.ServerManager, lm *backend.LibraryManager, im *backend.ImageManager, nav func(Route)) *FavoritesPage { +func NewFavoritesPage(sm *backend.ServerManager, pm *backend.PlaybackManager, lm *backend.LibraryManager, im *backend.ImageManager, nav func(Route)) *FavoritesPage { a := &FavoritesPage{ + pm: pm, lm: lm, sm: sm, im: im, @@ -66,6 +66,7 @@ func (a *FavoritesPage) createContainer() { func restoreFavoritesPage(saved *savedFavoritesPage) *FavoritesPage { a := &FavoritesPage{ + pm: saved.pm, lm: saved.lm, sm: saved.sm, im: saved.im, @@ -88,16 +89,13 @@ func (a *FavoritesPage) Route() Route { return FavoritesRoute() } -func (a *FavoritesPage) SetPlayAlbumCallback(cb func(string, int)) { - a.OnPlayAlbum = cb -} - func (a *FavoritesPage) Reload() { a.grid.Reset(a.lm.StarredIter()) } func (a *FavoritesPage) Save() SavedPage { return &savedFavoritesPage{ + pm: a.pm, sm: a.sm, im: a.im, lm: a.lm, @@ -142,9 +140,7 @@ func (a *FavoritesPage) doSearch(query string) { } func (a *FavoritesPage) onPlayAlbum(albumID string) { - if a.OnPlayAlbum != nil { - a.OnPlayAlbum(albumID, 0) - } + a.pm.PlayAlbum(albumID, 0) } func (a *FavoritesPage) onShowAlbumPage(albumID string) { @@ -161,6 +157,7 @@ func (a *FavoritesPage) CreateRenderer() fyne.WidgetRenderer { } type savedFavoritesPage struct { + pm *backend.PlaybackManager sm *backend.ServerManager im *backend.ImageManager lm *backend.LibraryManager diff --git a/ui/browsing/router.go b/ui/browsing/router.go index e95b453..d739b8c 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -93,7 +93,7 @@ func (r Router) CreatePage(rte Route) Page { case Artists: return NewArtistsGenresPage(false, r.App.ServerManager, r.OpenRoute) case Favorites: - return NewFavoritesPage(r.App.ServerManager, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute) + return NewFavoritesPage(r.App.ServerManager, r.App.PlaybackManager, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute) case Genre: return NewGenrePage(rte.Arg, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute) case Genres: From 28fc4271a3ee4addf113f9c222d54b7fabffbf0a Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 10 Feb 2023 19:02:08 -0800 Subject: [PATCH 03/12] clear selection on removing tracks from playlist --- ui/browsing/playlistpage.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index fee6f9c..635434d 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -119,6 +119,7 @@ func (a *PlaylistPage) loadAsync() { func (a *PlaylistPage) onRemoveSelectedFromPlaylist() { a.sm.Server.UpdatePlaylistTracks(a.playlistID, nil, a.tracklist.SelectedTrackIndexes()) + a.tracklist.UnselectAll() go a.Reload() } From 14e1bb0bf4e5d4d67e15a3cc54ea5964011ad141 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 10 Feb 2023 19:55:30 -0800 Subject: [PATCH 04/12] add select all shortcut to album and playlist pages --- ui/browsing/albumpage.go | 4 ++++ ui/browsing/browsingpane.go | 10 ++++++++++ ui/browsing/playlistpage.go | 4 ++++ ui/mainwindow.go | 3 +++ ui/widgets/tracklist.go | 5 +++++ 5 files changed, 26 insertions(+) diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index b5957c4..5e73ce6 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -108,6 +108,10 @@ func (a *AlbumPage) Tapped(*fyne.PointEvent) { a.tracklist.UnselectAll() } +func (a *AlbumPage) SelectAll() { + a.tracklist.SelectAll() +} + func (a *AlbumPage) onPlayTrackAt(tracknum int) { a.pm.PlayAlbum(a.albumID, tracknum) } diff --git a/ui/browsing/browsingpane.go b/ui/browsing/browsingpane.go index 278fa25..8a6670a 100644 --- a/ui/browsing/browsingpane.go +++ b/ui/browsing/browsingpane.go @@ -31,6 +31,10 @@ type Searchable interface { SearchWidget() fyne.Focusable } +type CanSelectAll interface { + SelectAll() +} + type CanShowNowPlaying interface { OnSongChange(song *subsonic.Child) } @@ -89,6 +93,12 @@ func (b *BrowsingPane) GetSearchBarIfAny() fyne.Focusable { return nil } +func (b *BrowsingPane) SelectAll() { + if s, ok := b.curPage.(CanSelectAll); ok { + s.SelectAll() + } +} + func (b *BrowsingPane) doSetPage(p Page) bool { if b.curPage != nil && b.curPage.Route() == p.Route() { return false diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 635434d..886d086 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -99,6 +99,10 @@ func (a *PlaylistPage) Tapped(*fyne.PointEvent) { a.tracklist.UnselectAll() } +func (a *PlaylistPage) SelectAll() { + a.tracklist.SelectAll() +} + func (a *PlaylistPage) onPlayTrackAt(tracknum int) { a.pm.PlayPlaylist(a.playlistID, tracknum) } diff --git a/ui/mainwindow.go b/ui/mainwindow.go index 399ad14..8c08a34 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -117,6 +117,9 @@ func (m *MainWindow) addShortcuts() { m.Window.Canvas().Focus(s) } }) + m.Canvas().AddShortcut(&fyne.ShortcutSelectAll{}, func(_ fyne.Shortcut) { + m.BrowsingPane.SelectAll() + }) m.Canvas().SetOnTypedKey(func(e *fyne.KeyEvent) { if e.Name == fyne.KeySpace { diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 32fde47..e3eb23f 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -170,6 +170,11 @@ func (t *Tracklist) SetNowPlaying(trackID string) { t.list.Refresh() } +func (t *Tracklist) SelectAll() { + t.selectionMgr.SelectAll() + t.Refresh() +} + func (t *Tracklist) UnselectAll() { t.selectionMgr.UnselectAll() t.Refresh() From dd2ecf620668001f2411b73734b5bfd6d806d8c7 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 10 Feb 2023 20:20:37 -0800 Subject: [PATCH 05/12] get rid of pointer to controller --- ui/browsing/albumpage.go | 4 ++-- ui/browsing/artistpage.go | 4 ++-- ui/browsing/playlistpage.go | 4 ++-- ui/browsing/router.go | 4 ++-- ui/controller/controller.go | 4 ++-- ui/mainwindow.go | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 5e73ce6..51ca39d 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -34,7 +34,7 @@ type albumPageState struct { pm *backend.PlaybackManager im *backend.ImageManager sm *backend.ServerManager - contr *controller.Controller + contr controller.Controller nav func(Route) } @@ -44,7 +44,7 @@ func NewAlbumPage( pm *backend.PlaybackManager, lm *backend.LibraryManager, im *backend.ImageManager, - contr *controller.Controller, + contr controller.Controller, nav func(Route), ) *AlbumPage { a := &AlbumPage{ diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index dc1da27..b4f66a5 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -27,7 +27,7 @@ type artistPageState struct { sm *backend.ServerManager im *backend.ImageManager nav func(Route) - contr *controller.Controller + contr controller.Controller } type ArtistPage struct { @@ -41,7 +41,7 @@ type ArtistPage struct { OnPlayAlbum func(string, int) } -func NewArtistPage(artistID string, sm *backend.ServerManager, im *backend.ImageManager, contr *controller.Controller, nav func(Route)) *ArtistPage { +func NewArtistPage(artistID string, sm *backend.ServerManager, im *backend.ImageManager, contr controller.Controller, nav func(Route)) *ArtistPage { a := &ArtistPage{artistPageState: artistPageState{ artistID: artistID, sm: sm, diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 886d086..9ef07c5 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -30,7 +30,7 @@ type PlaylistPage struct { type playlistPageState struct { playlistID string - contr *controller.Controller + contr controller.Controller sm *backend.ServerManager pm *backend.PlaybackManager im *backend.ImageManager @@ -39,7 +39,7 @@ type playlistPageState struct { func NewPlaylistPage( playlistID string, - contr *controller.Controller, + contr controller.Controller, sm *backend.ServerManager, pm *backend.PlaybackManager, im *backend.ImageManager, diff --git a/ui/browsing/router.go b/ui/browsing/router.go index d739b8c..b3d8cc5 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -67,13 +67,13 @@ type NavigationHandler interface { type Router struct { App *backend.App - Controller *controller.Controller + Controller controller.Controller Nav NavigationHandler pop util.PopUpProvider } -func NewRouter(app *backend.App, controller *controller.Controller, nav NavigationHandler) Router { +func NewRouter(app *backend.App, controller controller.Controller, nav NavigationHandler) Router { r := Router{ App: app, Controller: controller, diff --git a/ui/controller/controller.go b/ui/controller/controller.go index bed132a..69dd137 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -17,7 +17,7 @@ type Controller struct { App *backend.App } -func (m *Controller) ShowPopUpImage(img image.Image) { +func (m Controller) ShowPopUpImage(img image.Image) { im := canvas.NewImageFromImage(img) im.FillMode = canvas.ImageFillContain pop := widget.NewPopUp(im, m.MainWindow.Canvas()) @@ -41,7 +41,7 @@ func (m *Controller) ShowPopUpImage(img image.Image) { // Show dialog to prompt for playlist. // Depending on the results of that dialog, potentially create a new playlist // Add tracks to the user-specified playlist -func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { +func (m Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { pls, err := m.App.LibraryManager.GetUserOwnedPlaylists() if err != nil { // TODO: surface this error to user diff --git a/ui/mainwindow.go b/ui/mainwindow.go index 8c08a34..642d022 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -27,7 +27,7 @@ type MainWindow struct { App *backend.App Router browsing.Router - Controller *controller.Controller + Controller controller.Controller BrowsingPane *browsing.BrowsingPane BottomPanel *BottomPanel @@ -45,7 +45,7 @@ func NewMainWindow(fyneApp fyne.App, appName string, app *backend.App, size fyne BrowsingPane: browsing.NewBrowsingPane(app), } - m.Controller = &controller.Controller{ + m.Controller = controller.Controller{ MainWindow: m.Window, App: app, } From 5eda8264eb085f232c0156b24db0bbaf9a3ee7e2 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 11 Feb 2023 09:15:51 -0800 Subject: [PATCH 06/12] change styling of currently playing tracklist row --- ui/widgets/tracklist.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index e3eb23f..bef4e8d 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -34,12 +34,13 @@ type TrackRow struct { OnDoubleTapped func() OnTappedSecondary func(e *fyne.PointEvent, trackIdx int) + playingIcon fyne.CanvasObject selectionRect *canvas.Rectangle container *fyne.Container } -func NewTrackRow(layout *layouts.ColumnsLayout) *TrackRow { - t := &TrackRow{} +func NewTrackRow(layout *layouts.ColumnsLayout, playingIcon fyne.CanvasObject) *TrackRow { + t := &TrackRow{playingIcon: playingIcon} t.ExtendBaseWidget(t) t.num = widget.NewRichTextWithText("") t.num.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignTrailing @@ -73,11 +74,16 @@ func (t *TrackRow) Update(tr *subsonic.Child, isPlaying bool, rowNum int) { t.artist.Segments[0].(*widget.TextSegment).Text = tr.Artist t.dur.Segments[0].(*widget.TextSegment).Text = util.SecondsToTimeString(float64(tr.Duration)) - t.num.Segments[0].(*widget.TextSegment).Style.TextStyle = fyne.TextStyle{Bold: isPlaying} - t.name.Segments[0].(*widget.TextSegment).Style.TextStyle = fyne.TextStyle{Bold: isPlaying, Italic: isPlaying} - t.artist.Segments[0].(*widget.TextSegment).Style.TextStyle = fyne.TextStyle{Bold: isPlaying, Italic: isPlaying} + t.name.Segments[0].(*widget.TextSegment).Style.TextStyle = fyne.TextStyle{Bold: isPlaying} + t.artist.Segments[0].(*widget.TextSegment).Style.TextStyle = fyne.TextStyle{Bold: isPlaying} t.dur.Segments[0].(*widget.TextSegment).Style.TextStyle = fyne.TextStyle{Bold: isPlaying} + if isPlaying { + t.container.Objects[1].(*fyne.Container).Objects[0] = container.NewCenter(t.playingIcon) + } else { + t.container.Objects[1].(*fyne.Container).Objects[0] = t.num + } + t.Refresh() } @@ -136,10 +142,11 @@ func NewTracklist(tracks []*subsonic.Child) *Tracklist { t.selectionMgr = util.NewListSelectionManager(func() int { return len(t.Tracks) }) t.colLayout = layouts.NewColumnsLayout([]float32{35, -1, -1, 60}) t.hdr = NewListHeader([]ListColumn{{"#", true}, {"Title", false}, {"Artist", false}, {"Time", true}}, t.colLayout) + playingIcon := container.NewCenter(container.NewHBox(NewHSpace(2), widget.NewIcon(theme.MediaPlayIcon()))) t.list = widget.NewList( func() int { return len(t.Tracks) }, func() fyne.CanvasObject { - tr := NewTrackRow(t.colLayout) + tr := NewTrackRow(t.colLayout, playingIcon) tr.OnTapped = func() { t.onSelectTrack(tr.trackIdx) } tr.OnTappedSecondary = t.onShowContextMenu tr.OnDoubleTapped = func() { t.onPlayTrackAt(tr.trackIdx) } From 1cb273e086847f186cfc8fbed1f29a795d58abdb Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 11 Feb 2023 11:30:28 -0800 Subject: [PATCH 07/12] fix playlist page route --- ui/browsing/playlistpage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 9ef07c5..c41daf6 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -79,7 +79,7 @@ func (a *PlaylistPage) Save() SavedPage { } func (a *PlaylistPage) Route() Route { - return AlbumRoute(a.playlistID) + return PlaylistRoute(a.playlistID) } func (a *PlaylistPage) OnSongChange(song *subsonic.Child) { From 7d5677057c8fb6bfde9d9ebc48672fd1a6b5b770 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 11 Feb 2023 12:00:33 -0800 Subject: [PATCH 08/12] add Now Playing page to show play queue --- backend/playbackmanager.go | 14 +++++ ui/browsing/nowplayingpage.go | 102 ++++++++++++++++++++++++++++++++++ ui/browsing/router.go | 10 +++- ui/mainwindow.go | 3 + ui/widgets/tracklist.go | 33 ++++++----- 5 files changed, 145 insertions(+), 17 deletions(-) create mode 100644 ui/browsing/nowplayingpage.go diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 94ea457..334f869 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -161,6 +161,20 @@ func (p *PlaybackManager) PlayFromBeginning() error { return p.player.PlayFromBeginning() } +func (p *PlaybackManager) PlayTrackAt(idx int) error { + return p.player.PlayTrackAt(idx) +} + +func (p *PlaybackManager) GetPlayQueue() []*subsonic.Child { + pq := make([]*subsonic.Child, len(p.playQueue)) + copy(pq, p.playQueue) + return pq +} + +func (p *PlaybackManager) RemoveTracksFromQueue(trackIdxs []int) { + // TODO +} + func (p *PlaybackManager) checkScrobble(playDur time.Duration) { if playDur.Seconds() < 0.1 || p.curTrackTime < 0.1 { return diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go new file mode 100644 index 0000000..70a0fab --- /dev/null +++ b/ui/browsing/nowplayingpage.go @@ -0,0 +1,102 @@ +package browsing + +import ( + "supersonic/backend" + "supersonic/ui/controller" + "supersonic/ui/layouts" + "supersonic/ui/widgets" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" + "github.com/dweymouth/go-subsonic/subsonic" +) + +type NowPlayingPage struct { + widget.BaseWidget + + nowPlayingPageState + + title *widget.RichText + tracklist *widgets.Tracklist + nowPlayingID string + container *fyne.Container +} + +type nowPlayingPageState struct { + contr controller.Controller + sm *backend.ServerManager + pm *backend.PlaybackManager + nav func(Route) +} + +func NewNowPlayingPage( + contr controller.Controller, + sm *backend.ServerManager, + pm *backend.PlaybackManager, + nav func(Route), +) *NowPlayingPage { + a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{contr: contr, sm: sm, pm: pm, nav: nav}} + a.ExtendBaseWidget(a) + a.tracklist = widgets.NewTracklist(nil) + a.tracklist.AutoNumber = true + a.tracklist.DisablePlaybackMenu = true + a.tracklist.OnPlayTrackAt = a.onPlayTrackAt + a.tracklist.OnAddToPlaylist = a.contr.DoAddTracksToPlaylistWorkflow + a.title = widget.NewRichTextWithText("Now Playing") + a.title.Segments[0].(*widget.TextSegment).Style.SizeName = widget.RichTextStyleHeading.SizeName + a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15}, + container.NewBorder(a.title, nil, nil, nil, a.tracklist)) + a.loadAsync() + return a +} + +func (a *NowPlayingPage) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(a.container) +} + +func (a *NowPlayingPage) Save() SavedPage { + nps := a.nowPlayingPageState + return &nps +} + +func (a *NowPlayingPage) Route() Route { + return NowPlayingRoute() +} + +func (a *NowPlayingPage) Tapped(*fyne.PointEvent) { + a.tracklist.UnselectAll() +} + +func (a *NowPlayingPage) SelectAll() { + a.tracklist.SelectAll() +} + +func (a *NowPlayingPage) OnSongChange(song *subsonic.Child) { + if song == nil { + a.nowPlayingID = "" + } else { + a.nowPlayingID = song.ID + } + a.tracklist.SetNowPlaying(a.nowPlayingID) +} + +func (a *NowPlayingPage) Reload() { + a.loadAsync() +} + +func (a *NowPlayingPage) onPlayTrackAt(tracknum int) { + _ = a.pm.PlayTrackAt(tracknum) +} + +func (a *NowPlayingPage) loadAsync() { + go func() { + queue := a.pm.GetPlayQueue() + a.tracklist.Tracks = queue + a.tracklist.SetNowPlaying(a.nowPlayingID) + }() +} + +func (s *nowPlayingPageState) Restore() Page { + return NewNowPlayingPage(s.contr, s.sm, s.pm, s.nav) +} diff --git a/ui/browsing/router.go b/ui/browsing/router.go index b3d8cc5..ae006b2 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -3,7 +3,6 @@ package browsing import ( "supersonic/backend" "supersonic/ui/controller" - "supersonic/ui/util" ) type PageName int @@ -17,6 +16,7 @@ const ( Genre Genres Favorites + NowPlaying Playlist Playlists ) @@ -61,6 +61,10 @@ func ArtistsRoute() Route { return Route{Page: Artists} } +func NowPlayingRoute() Route { + return Route{Page: NowPlaying} +} + type NavigationHandler interface { SetPage(Page) } @@ -69,8 +73,6 @@ type Router struct { App *backend.App Controller controller.Controller Nav NavigationHandler - - pop util.PopUpProvider } func NewRouter(app *backend.App, controller controller.Controller, nav NavigationHandler) Router { @@ -98,6 +100,8 @@ func (r Router) CreatePage(rte Route) Page { return NewGenrePage(rte.Arg, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute) case Genres: return NewArtistsGenresPage(true, r.App.ServerManager, r.OpenRoute) + case NowPlaying: + return NewNowPlayingPage(r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.OpenRoute) case Playlist: return NewPlaylistPage(rte.Arg, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager, r.OpenRoute) case Playlists: diff --git a/ui/mainwindow.go b/ui/mainwindow.go index 642d022..44a5966 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -82,6 +82,9 @@ func (m *MainWindow) PromptForFirstServer(cb func(string, string, string, string } func (m *MainWindow) addNavigationButtons() { + m.BrowsingPane.AddNavigationButton(res.ResHeadphonesInvertPng, func() { + m.Router.OpenRoute(browsing.NowPlayingRoute()) + }) m.BrowsingPane.AddNavigationButton(res.ResHeartFilledInvertPng, func() { m.Router.OpenRoute(browsing.FavoritesRoute()) }) diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index bef4e8d..d7b14aa 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -119,7 +119,8 @@ type Tracklist struct { Tracks []*subsonic.Child AutoNumber bool // must be set before the context menu is shown for the first time - AuxiliaryMenuItems []*fyne.MenuItem + AuxiliaryMenuItems []*fyne.MenuItem + DisablePlaybackMenu bool // user action callbacks OnPlayTrackAt func(int) @@ -216,23 +217,27 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) { t.selectionMgr.Select(trackIdx) t.Refresh() if t.ctxMenu == nil { - t.ctxMenu = fyne.NewMenu("", - fyne.NewMenuItem("Play", func() { - if t.OnPlaySelection != nil { - t.OnPlaySelection(t.selectedTracks()) - } - }), - fyne.NewMenuItem("Add to queue", func() { - if t.OnPlaySelection != nil { - t.OnAddToQueue(t.selectedTracks()) - } - }), + t.ctxMenu = fyne.NewMenu("") + if !t.DisablePlaybackMenu { + t.ctxMenu.Items = append(t.ctxMenu.Items, + fyne.NewMenuItem("Play", func() { + if t.OnPlaySelection != nil { + t.OnPlaySelection(t.selectedTracks()) + } + })) + t.ctxMenu.Items = append(t.ctxMenu.Items, + fyne.NewMenuItem("Add to queue", func() { + if t.OnPlaySelection != nil { + t.OnAddToQueue(t.selectedTracks()) + } + })) + } + t.ctxMenu.Items = append(t.ctxMenu.Items, fyne.NewMenuItem("Add to playlist...", func() { if t.OnAddToPlaylist != nil { t.OnAddToPlaylist(t.selectedTrackIDs()) } - }), - ) + })) if len(t.AuxiliaryMenuItems) > 0 { t.ctxMenu.Items = append(t.ctxMenu.Items, fyne.NewMenuItemSeparator()) t.ctxMenu.Items = append(t.ctxMenu.Items, t.AuxiliaryMenuItems...) From b667e2cd07b2773e06b056167aef71a172cae8cf Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 11 Feb 2023 13:07:02 -0800 Subject: [PATCH 09/12] now playing page can remove tracks from play queue --- backend/playbackmanager.go | 34 ++++++++++++++++++++++++++++++++-- player/player.go | 8 ++++++++ ui/browsing/nowplayingpage.go | 9 +++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 334f869..5ee3168 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -64,6 +64,7 @@ func NewPlaybackManager(ctx context.Context, s *ServerManager, p *player.Player) pm.checkScrobble(pm.playTimeStopwatch.Elapsed()) pm.playTimeStopwatch.Reset() pm.stopPollTimePos() + pm.doUpdateTimePos() for _, cb := range pm.onSongChange { cb(nil) } @@ -171,14 +172,43 @@ func (p *PlaybackManager) GetPlayQueue() []*subsonic.Child { return pq } +// trackIdxs must be sorted func (p *PlaybackManager) RemoveTracksFromQueue(trackIdxs []int) { - // TODO + newQueue := make([]*subsonic.Child, 0, len(p.playQueue)-len(trackIdxs)) + rmCount := 0 + rmIdx := 0 + for i, tr := range p.playQueue { + if rmIdx < len(trackIdxs) && trackIdxs[rmIdx] == i { + // removing this track + rmIdx++ + if err := p.player.RemoveTrackAt(i - rmCount); err == nil { + rmCount++ + } else { + log.Printf("error removing track: %v", err.Error()) + // did not remove this track + newQueue = append(newQueue, tr) + } + } else { + // not removing this track + newQueue = append(newQueue, tr) + } + } + p.playQueue = newQueue + p.nowPlayingIdx = p.player.GetStatus().PlaylistPos + // fire on song change callbacks in case the playing track was removed + // TODO: only call this if the playing track actually was removed + for _, cb := range p.onSongChange { + cb(p.NowPlaying()) + } } func (p *PlaybackManager) checkScrobble(playDur time.Duration) { - if playDur.Seconds() < 0.1 || p.curTrackTime < 0.1 { + if len(p.playQueue) == 0 || p.nowPlayingIdx < 0 { return } + if playDur.Seconds() < 0.1 || p.curTrackTime < 0.1 { + return // ignore spurious onTrackChange callbacks + } song := p.playQueue[p.nowPlayingIdx] if playDur.Seconds()/p.curTrackTime > ScrobbleThreshold { log.Printf("Scrobbling %q", song.Title) diff --git a/player/player.go b/player/player.go index 26e5108..9574458 100644 --- a/player/player.go +++ b/player/player.go @@ -131,6 +131,14 @@ func (p *Player) PlayFile(url string) error { return err } +// Removes the item at the given index from the internal playqueue. +func (p *Player) RemoveTrackAt(idx int) error { + if p.mpv == nil { + return ErrUnitialized + } + return p.mpv.Command([]string{"playlist-remove", strconv.Itoa(idx)}) +} + // Stops playback and clears the play queue. func (p *Player) Stop() error { if p.mpv == nil { diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index 70a0fab..55f9df1 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -43,6 +43,9 @@ func NewNowPlayingPage( a.tracklist.DisablePlaybackMenu = true a.tracklist.OnPlayTrackAt = a.onPlayTrackAt a.tracklist.OnAddToPlaylist = a.contr.DoAddTracksToPlaylistWorkflow + a.tracklist.AuxiliaryMenuItems = []*fyne.MenuItem{ + fyne.NewMenuItem("Remove from queue", a.onRemoveSelectedFromQueue), + } a.title = widget.NewRichTextWithText("Now Playing") a.title.Segments[0].(*widget.TextSegment).Style.SizeName = widget.RichTextStyleHeading.SizeName a.container = container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 5, PadBottom: 15}, @@ -89,6 +92,12 @@ func (a *NowPlayingPage) onPlayTrackAt(tracknum int) { _ = a.pm.PlayTrackAt(tracknum) } +func (a *NowPlayingPage) onRemoveSelectedFromQueue() { + a.pm.RemoveTracksFromQueue(a.tracklist.SelectedTrackIndexes()) + a.tracklist.UnselectAll() + go a.Reload() +} + func (a *NowPlayingPage) loadAsync() { go func() { queue := a.pm.GetPlayQueue() From 91a8b0d9b78fa7784887a603a261a933eb17db4e Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 11 Feb 2023 13:23:57 -0800 Subject: [PATCH 10/12] updates to README features --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ef2f2c4..79e666d 100644 --- a/README.md +++ b/README.md @@ -13,21 +13,22 @@ Slightly outdated screenshots of Supersonic running against the Navidrome Date: Sat, 11 Feb 2023 13:27:29 -0800 Subject: [PATCH 11/12] add TODO comment for scrobbling when removing currently playing track --- backend/playbackmanager.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 5ee3168..ffe8956 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -180,6 +180,8 @@ func (p *PlaybackManager) RemoveTracksFromQueue(trackIdxs []int) { for i, tr := range p.playQueue { if rmIdx < len(trackIdxs) && trackIdxs[rmIdx] == i { // removing this track + // TODO: if we are removing the currently playing track, + // we need to scrobble it if it played for more than the scrobble threshold rmIdx++ if err := p.player.RemoveTrackAt(i - rmCount); err == nil { rmCount++ From 4cc52d4135b7a884a3a2fc8742c0055095a7f68a Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 11 Feb 2023 13:46:46 -0800 Subject: [PATCH 12/12] ensure we unpause in player.PlayTrackAt --- player/player.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/player/player.go b/player/player.go index 9574458..571104a 100644 --- a/player/player.go +++ b/player/player.go @@ -238,6 +238,9 @@ func (p *Player) PlayFromBeginning() error { // Start playback from the specified track index in the play queue. func (p *Player) PlayTrackAt(idx int) error { err := p.mpv.Command([]string{"playlist-play-index", strconv.Itoa(idx)}) + if p.GetStatus().State == Paused { + err = p.setPaused(false) + } if err == nil { p.setState(Playing) }