From edea337e7ff39aaf73336e1afd24aa3cba42f3e3 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 8 Feb 2023 16:48:38 -0800 Subject: [PATCH] add context menu to tracklist --- backend/playbackmanager.go | 4 ++ ui/browsing/albumpage.go | 20 ++++++- ui/browsing/playlistpage.go | 11 ++++ ui/browsing/router.go | 2 +- ui/widgets/tracklist.go | 107 ++++++++++++++++++++++++++---------- 5 files changed, 113 insertions(+), 31 deletions(-) diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 826f6c5..70feab3 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -157,6 +157,10 @@ func (p *PlaybackManager) PlayPlaylist(playlistID string, firstTrack int) error return p.player.PlayTrackAt(firstTrack) } +func (p *PlaybackManager) PlayFromBeginning() error { + return p.player.PlayFromBeginning() +} + func (p *PlaybackManager) checkScrobble(playDur time.Duration) { if playDur.Seconds() < 0.1 || p.curTrackTime < 0.1 { return diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 4677074..e29ff8e 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -21,6 +21,7 @@ type AlbumPage struct { albumID string sm *backend.ServerManager + pm *backend.PlaybackManager im *backend.ImageManager lm *backend.LibraryManager nav func(Route) @@ -36,19 +37,28 @@ type AlbumPage struct { func NewAlbumPage( albumID string, sm *backend.ServerManager, + pm *backend.PlaybackManager, lm *backend.LibraryManager, im *backend.ImageManager, popUpProvider util.PopUpProvider, nav func(Route), ) *AlbumPage { - a := &AlbumPage{albumID: albumID, sm: sm, lm: lm, im: im, nav: nav, popUpProvider: popUpProvider} + a := &AlbumPage{albumID: albumID, sm: sm, pm: pm, lm: lm, im: im, nav: nav, popUpProvider: popUpProvider} a.ExtendBaseWidget(a) a.header = NewAlbumPageHeader(a) a.tracklist = widgets.NewTracklist(nil) + // connect tracklist actions a.tracklist.OnPlayTrackAt = a.onPlayTrackAt + a.tracklist.OnAddToQueue = func(tracks []*subsonic.Child) { a.pm.LoadTracks(tracks, true) } + a.tracklist.OnPlaySelection = func(tracks []*subsonic.Child) { + a.pm.LoadTracks(tracks, false) + a.pm.PlayFromBeginning() + } + a.container = container.NewBorder( container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 15, PadBottom: 10}, a.header), nil, nil, nil, container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadBottom: 15}, a.tracklist)) + a.loadAsync() return a } @@ -65,6 +75,7 @@ func (a *AlbumPage) Save() SavedPage { return &savedAlbumPage{ albumID: a.albumID, lm: a.lm, + pm: a.pm, im: a.im, nav: a.nav, popUpProvider: a.popUpProvider, @@ -88,6 +99,10 @@ func (a *AlbumPage) Reload() { a.loadAsync() } +func (a *AlbumPage) Tapped(*fyne.PointEvent) { + a.tracklist.UnselectAll() +} + func (a *AlbumPage) onPlayTrackAt(tracknum int) { if a.OnPlayAlbum != nil { a.OnPlayAlbum(a.albumID, tracknum) @@ -227,6 +242,7 @@ func formatMiscLabelStr(a *subsonic.AlbumID3) string { type savedAlbumPage struct { albumID string lm *backend.LibraryManager + pm *backend.PlaybackManager im *backend.ImageManager sm *backend.ServerManager popUpProvider util.PopUpProvider @@ -234,5 +250,5 @@ type savedAlbumPage struct { } func (s *savedAlbumPage) Restore() Page { - return NewAlbumPage(s.albumID, s.sm, s.lm, s.im, s.popUpProvider, s.nav) + return NewAlbumPage(s.albumID, s.sm, s.pm, s.lm, s.im, s.popUpProvider, s.nav) } diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index ea01e9a..178ad4a 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -42,7 +42,14 @@ func NewPlaylistPage( a.header = NewPlaylistPageHeader(a) a.tracklist = widgets.NewTracklist(nil) a.tracklist.AutoNumber = true + // connect tracklist actions a.tracklist.OnPlayTrackAt = a.onPlayTrackAt + a.tracklist.OnAddToQueue = func(tracks []*subsonic.Child) { a.pm.LoadTracks(tracks, true) } + a.tracklist.OnPlaySelection = func(tracks []*subsonic.Child) { + a.pm.LoadTracks(tracks, false) + a.pm.PlayFromBeginning() + } + a.container = container.NewBorder( container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 15, PadBottom: 10}, a.header), nil, nil, nil, container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadBottom: 15}, a.tracklist)) @@ -81,6 +88,10 @@ func (a *PlaylistPage) Reload() { a.loadAsync() } +func (a *PlaylistPage) Tapped(*fyne.PointEvent) { + a.tracklist.UnselectAll() +} + func (a *PlaylistPage) onPlayTrackAt(tracknum int) { a.pm.PlayPlaylist(a.playlistID, tracknum) } diff --git a/ui/browsing/router.go b/ui/browsing/router.go index 4ec1f9e..17d14d1 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -100,7 +100,7 @@ func (p *popUpProvider) WindowSize() fyne.Size { func (r Router) CreatePage(rte Route) Page { switch rte.Page { case Album: - return NewAlbumPage(rte.Arg, r.App.ServerManager, r.App.LibraryManager, r.App.ImageManager, r.pop, r.OpenRoute) + return NewAlbumPage(rte.Arg, r.App.ServerManager, r.App.PlaybackManager, r.App.LibraryManager, r.App.ImageManager, r.pop, r.OpenRoute) case Albums: return NewAlbumsPage("Albums", rte.Arg, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute) case Artist: diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 29e3b65..654c7aa 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -19,23 +19,23 @@ import ( type TrackRow struct { widget.BaseWidget - trackID string - prevTrackID string - prevIsPlaying bool - - tappedAt int64 // unixMillis + // internal state + trackIdx int + trackID string + isPlaying bool + tappedAt int64 // unixMillis num *widget.RichText name *widget.RichText artist *widget.RichText dur *widget.RichText - OnTapped func() - OnDoubleTapped func() + OnTapped func() + OnDoubleTapped func() + OnTappedSecondary func(e *fyne.PointEvent, trackIdx int) selectionRect *canvas.Rectangle - - container *fyne.Container + container *fyne.Container } func NewTrackRow(layout *layouts.ColumnsLayout) *TrackRow { @@ -59,11 +59,10 @@ func NewTrackRow(layout *layouts.ColumnsLayout) *TrackRow { } func (t *TrackRow) Update(tr *subsonic.Child, isPlaying bool, rowNum int) { - if tr.ID == t.prevTrackID && isPlaying == t.prevIsPlaying { + if tr.ID == t.trackID && isPlaying == t.isPlaying { return } - t.prevTrackID = t.trackID - t.prevIsPlaying = isPlaying + t.isPlaying = isPlaying t.trackID = tr.ID if rowNum < 0 { @@ -102,38 +101,55 @@ func (t *TrackRow) Tapped(*fyne.PointEvent) { } } +func (t *TrackRow) TappedSecondary(e *fyne.PointEvent) { + if t.OnTappedSecondary != nil { + t.OnTappedSecondary(e, t.trackIdx) + } +} + type Tracklist struct { widget.BaseWidget - Tracks []*subsonic.Child - AutoNumber bool - OnPlayTrackAt func(int) - SelectionMgr util.ListSelectionManager + Tracks []*subsonic.Child + AutoNumber bool + // user action callbacks + OnPlayTrackAt func(int) + OnPlaySelection func(tracks []*subsonic.Child) + OnAddToQueue func(trackIDs []*subsonic.Child) + OnAddToPlaylist func(trackIDs []*subsonic.Child) + + selectionMgr util.ListSelectionManager nowPlayingIdx int colLayout *layouts.ColumnsLayout hdr *ListHeader list *widget.List + ctxMenu *fyne.Menu container *fyne.Container } func NewTracklist(tracks []*subsonic.Child) *Tracklist { t := &Tracklist{Tracks: tracks, nowPlayingIdx: -1} t.ExtendBaseWidget(t) - t.SelectionMgr = util.NewListSelectionManager(func() int { return len(t.Tracks) }) + 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) t.list = widget.NewList( func() int { return len(t.Tracks) }, - func() fyne.CanvasObject { return NewTrackRow(t.colLayout) }, + func() fyne.CanvasObject { + tr := NewTrackRow(t.colLayout) + tr.OnTapped = func() { t.onSelectTrack(tr.trackIdx) } + tr.OnTappedSecondary = t.onShowContextMenu + tr.OnDoubleTapped = func() { t.onPlayTrackAt(tr.trackIdx) } + return tr + }, func(itemID widget.ListItemID, item fyne.CanvasObject) { tr := item.(*TrackRow) - tr.OnTapped = func() { t.onSelectTrack(itemID) } - tr.OnDoubleTapped = func() { t.onPlayTrackAt(itemID) } - tr.selectionRect.Hidden = !t.SelectionMgr.IsSelected(itemID) - i := itemID + 1 - if !t.AutoNumber { - i = -1 // signal that we want to use the track num. + tr.trackIdx = itemID + tr.selectionRect.Hidden = !t.selectionMgr.IsSelected(itemID) + i := -1 // signal that we want to display the actual track num. + if t.AutoNumber { + i = itemID + 1 } tr.Update(t.Tracks[itemID], itemID == t.nowPlayingIdx, i) }) @@ -152,6 +168,11 @@ func (t *Tracklist) SetNowPlaying(trackID string) { t.list.Refresh() } +func (t *Tracklist) UnselectAll() { + t.selectionMgr.UnselectAll() + t.Refresh() +} + func (t *Tracklist) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(t.container) } @@ -165,14 +186,44 @@ func (t *Tracklist) onPlayTrackAt(idx int) { func (t *Tracklist) onSelectTrack(idx int) { if d, ok := fyne.CurrentApp().Driver().(desktop.Driver); ok { if d.ActiveKeyModifiers()&os.ControlModifier != 0 { - t.SelectionMgr.SelectAddOrRemove(idx) + t.selectionMgr.SelectAddOrRemove(idx) } else if (d.ActiveKeyModifiers() & fyne.KeyModifierShift) != 0 { - t.SelectionMgr.SelectRange(idx) + t.selectionMgr.SelectRange(idx) } else { - t.SelectionMgr.Select(idx) + t.selectionMgr.Select(idx) } } else { - t.SelectionMgr.Select(idx) + t.selectionMgr.Select(idx) } t.list.Refresh() } + +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()) + } + }), + //fyne.NewMenuItem("Add to playlist...", func() {}), + ) + } + widget.ShowPopUpMenuAtPosition(t.ctxMenu, fyne.CurrentApp().Driver().CanvasForObject(t), e.AbsolutePosition) +} + +func (t *Tracklist) selectedTracks() []*subsonic.Child { + sel := t.selectionMgr.GetSelection() + tracks := make([]*subsonic.Child, 0, len(sel)) + for _, idx := range sel { + tracks = append(tracks, t.Tracks[idx]) + } + return tracks +}