From 6963a5de8e3c7ad7a279601d30b78f87beceb5ce Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 6 Feb 2023 17:18:27 -0800 Subject: [PATCH 1/9] add selection to tracklist --- go.mod | 2 +- go.sum | 4 +- ui/mainwindow.go | 9 +- ui/{ => os}/keymodifiers_darwin.go | 6 +- ui/{ => os}/keymodifiers_default.go | 2 +- ui/util/listselectionmanager.go | 154 ++++++++++++++++++++++++++++ ui/widgets/tracklist.go | 54 ++++++++-- 7 files changed, 214 insertions(+), 17 deletions(-) rename ui/{ => os}/keymodifiers_darwin.go (73%) rename ui/{ => os}/keymodifiers_default.go (92%) create mode 100644 ui/util/listselectionmanager.go diff --git a/go.mod b/go.mod index 67534a0..86d6d4f 100644 --- a/go.mod +++ b/go.mod @@ -43,4 +43,4 @@ require ( honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect ) -replace fyne.io/fyne/v2 v2.2.4 => github.com/dweymouth/fyne/v2 v2.2.5-0.20230119024415-238e09217d09 +replace fyne.io/fyne/v2 v2.2.4 => github.com/dweymouth/fyne/v2 v2.2.5-0.20230207011038-d2ef06e09a93 diff --git a/go.sum b/go.sum index 9145ccc..e97a078 100644 --- a/go.sum +++ b/go.sum @@ -70,8 +70,8 @@ github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7h github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dweymouth/fyne/v2 v2.2.5-0.20230119024415-238e09217d09 h1:huJvmT36/E7MsuAVILCK7VPFd0UiPQDpphn215e1rCo= -github.com/dweymouth/fyne/v2 v2.2.5-0.20230119024415-238e09217d09/go.mod h1:MBoGuHzLLSXdQOWFAwWhIhYTEMp33zqtGCReSWhaQTA= +github.com/dweymouth/fyne/v2 v2.2.5-0.20230207011038-d2ef06e09a93 h1:4GGNszUwZhcneapbaBO+rbHtmCjKjZXHKPCqFXQPg4E= +github.com/dweymouth/fyne/v2 v2.2.5-0.20230207011038-d2ef06e09a93/go.mod h1:MBoGuHzLLSXdQOWFAwWhIhYTEMp33zqtGCReSWhaQTA= github.com/dweymouth/go-subsonic v0.0.0-20221214005741-bd8048fa1863 h1:bOWMpFJ9zY839T1EngQ5nxVCiMlatPtpGkg/yHK6szg= github.com/dweymouth/go-subsonic v0.0.0-20221214005741-bd8048fa1863/go.mod h1:fUez6NFiEJiQTZizZ1BThZr5GJXAbigzGYjEPNm4tdI= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= diff --git a/ui/mainwindow.go b/ui/mainwindow.go index adcc4c9..0111a6b 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -4,6 +4,7 @@ import ( "supersonic/backend" "supersonic/res" "supersonic/ui/browsing" + "supersonic/ui/os" "supersonic/ui/widgets" "fyne.io/fyne/v2" @@ -14,10 +15,10 @@ import ( ) var ( - ShortcutBack = desktop.CustomShortcut{KeyName: fyne.KeyLeft, Modifier: AltModifier} - ShortcutForward = desktop.CustomShortcut{KeyName: fyne.KeyRight, Modifier: AltModifier} - ShortcutReload = desktop.CustomShortcut{KeyName: fyne.KeyR, Modifier: ControlModifier} - ShortcutSearch = desktop.CustomShortcut{KeyName: fyne.KeyF, Modifier: ControlModifier} + ShortcutBack = desktop.CustomShortcut{KeyName: fyne.KeyLeft, Modifier: os.AltModifier} + ShortcutForward = desktop.CustomShortcut{KeyName: fyne.KeyRight, Modifier: os.AltModifier} + ShortcutReload = desktop.CustomShortcut{KeyName: fyne.KeyR, Modifier: os.ControlModifier} + ShortcutSearch = desktop.CustomShortcut{KeyName: fyne.KeyF, Modifier: os.ControlModifier} ) type MainWindow struct { diff --git a/ui/keymodifiers_darwin.go b/ui/os/keymodifiers_darwin.go similarity index 73% rename from ui/keymodifiers_darwin.go rename to ui/os/keymodifiers_darwin.go index a6b20a1..2d3423b 100644 --- a/ui/keymodifiers_darwin.go +++ b/ui/os/keymodifiers_darwin.go @@ -1,8 +1,10 @@ //go:build darwin -package ui +package os -import "fyne.io/fyne/v2" +import ( + "fyne.io/fyne/v2" +) var ( ControlModifier = fyne.KeyModifierSuper diff --git a/ui/keymodifiers_default.go b/ui/os/keymodifiers_default.go similarity index 92% rename from ui/keymodifiers_default.go rename to ui/os/keymodifiers_default.go index 24b75e2..20ffcf7 100644 --- a/ui/keymodifiers_default.go +++ b/ui/os/keymodifiers_default.go @@ -1,6 +1,6 @@ //go:build !darwin -package ui +package os import "fyne.io/fyne/v2" diff --git a/ui/util/listselectionmanager.go b/ui/util/listselectionmanager.go new file mode 100644 index 0000000..64216bd --- /dev/null +++ b/ui/util/listselectionmanager.go @@ -0,0 +1,154 @@ +package util + +type ListSelectionManager struct { + lastSelectedRow int + numSelected int + selected BitSet + len func() int +} + +func NewListSelectionManager(lenFn func() int) ListSelectionManager { + return ListSelectionManager{lastSelectedRow: -1, len: lenFn} +} + +// If the given row is not selected, reset the selection +// and select only the given row. +func (l *ListSelectionManager) Select(row int) { + if row < 0 || l.selected.IsSet(uint(row)) { + return + } + l.UnselectAll() + l.selectAdd(row) +} + +// If it is not selected, add the given row to the selection. +// If it is selected, unselet it. +func (l *ListSelectionManager) SelectAddOrRemove(row int) { + if row < 0 { + return + } + if l.selected.IsSet(uint(row)) { + // find new last selected row + if row == l.lastSelectedRow { + for i := row - 1; i >= -1; i++ { + if i == -1 { + l.lastSelectedRow = i + } else if l.selected.IsSet(uint(i)) { + l.lastSelectedRow = i + break + } + } + } + l.selected.Unset(uint(row)) + l.numSelected -= 1 + return + } + + l.selectAdd(row) +} + +func (l *ListSelectionManager) selectAdd(row int) { + l.numSelected += 1 + l.selected.Set(uint(row)) + if row > l.lastSelectedRow { + l.lastSelectedRow = row + } +} + +// Select a range between the given row and the furthest-down +// row that is currently selected (which may be above the given row) +// Note: this is modeled after what, as far as I can tell, is Gmail's selection behavior +func (l *ListSelectionManager) SelectRange(row int) { + if row < 0 || l.selected.IsSet(uint(row)) { + return + } + if l.numSelected == 0 { + l.selectAdd(row) + return + } + m := maxInt(row, l.lastSelectedRow) + for i := minInt(l.lastSelectedRow, row); i <= m; i++ { + l.selectAdd(i) + } +} + +func (l *ListSelectionManager) SelectAll() { + for i := 0; i < l.len(); i++ { + l.selectAdd(i) + } +} + +func (l *ListSelectionManager) UnselectAll() { + l.selected = nil + l.numSelected = 0 + l.lastSelectedRow = -1 +} + +func (l *ListSelectionManager) IsSelected(row int) bool { + return row >= 0 && l.selected.IsSet(uint(row)) +} + +func (l *ListSelectionManager) GetSelection() []int { + var sel []int + for i := 0; i < l.len(); i++ { + if l.selected.IsSet(uint(i)) { + sel = append(sel, i) + } + if len(sel) == l.numSelected { + break + } + } + return sel +} + +func (l *ListSelectionManager) AreAllSelected() bool { + return l.numSelected == l.len() +} + +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 +} + +// BitSet implementation from +// https://stackoverflow.com/questions/2311373/how-to-implement-bitset-with-go + +const uint64size = 64 + +// BitSet is a set of bits that can be set, cleared and queried. +type BitSet []uint64 + +// Set ensures that the given bit is set in the BitSet. +func (s *BitSet) Set(i uint) { + if len(*s) < int(i/uint64size+1) { + r := make([]uint64, i/uint64size+1) + copy(r, *s) + *s = r + } + (*s)[i/uint64size] |= 1 << (i % uint64size) +} + +// Unset ensures that the given bit is cleared (not set) in the BitSet. +func (s *BitSet) Unset(i uint) { + if len(*s) >= int(i/uint64size+1) { + (*s)[i/uint64size] &^= 1 << (i % uint64size) + } +} + +// IsSet returns true if the given bit is set, false if it is cleared. +func (s *BitSet) IsSet(i uint) bool { + idx := i / uint64size + if idx >= uint(len(*s)) { + return false + } + return (*s)[i/uint64size]&(1<<(i%uint64size)) != 0 +} diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index d95011b..29e3b65 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -3,10 +3,15 @@ package widgets import ( "strconv" "supersonic/ui/layouts" + "supersonic/ui/os" "supersonic/ui/util" + "time" "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/go-subsonic" ) @@ -18,18 +23,21 @@ type TrackRow struct { prevTrackID string prevIsPlaying bool + tappedAt int64 // unixMillis + num *widget.RichText name *widget.RichText artist *widget.RichText dur *widget.RichText + OnTapped func() OnDoubleTapped func() + selectionRect *canvas.Rectangle + container *fyne.Container } -var _ fyne.DoubleTappable = (*TrackRow)(nil) - func NewTrackRow(layout *layouts.ColumnsLayout) *TrackRow { t := &TrackRow{} t.ExtendBaseWidget(t) @@ -42,8 +50,11 @@ func NewTrackRow(layout *layouts.ColumnsLayout) *TrackRow { t.dur = widget.NewRichTextWithText("") t.dur.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignTrailing - t.container = container.New(layout, - t.num, t.name, t.artist, t.dur) + t.selectionRect = canvas.NewRectangle(theme.SelectionColor()) + t.selectionRect.Hidden = true + t.container = container.NewMax(t.selectionRect, + container.New(layout, + t.num, t.name, t.artist, t.dur)) return t } @@ -75,9 +86,19 @@ func (t *TrackRow) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(t.container) } -func (t *TrackRow) DoubleTapped(*fyne.PointEvent) { - if t.OnDoubleTapped != nil { - t.OnDoubleTapped() +// We implement our own double tapping so that the Tapped behavior +// can be triggered instantly. +func (t *TrackRow) Tapped(*fyne.PointEvent) { + prevTap := t.tappedAt + t.tappedAt = time.Now().UnixMilli() + if t.tappedAt-prevTap < 300 { + if t.OnDoubleTapped != nil { + t.OnDoubleTapped() + } + } else { + if t.OnTapped != nil { + t.OnTapped() + } } } @@ -87,6 +108,7 @@ type Tracklist struct { Tracks []*subsonic.Child AutoNumber bool OnPlayTrackAt func(int) + SelectionMgr util.ListSelectionManager nowPlayingIdx int colLayout *layouts.ColumnsLayout @@ -98,6 +120,7 @@ type Tracklist struct { 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.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( @@ -105,7 +128,9 @@ func NewTracklist(tracks []*subsonic.Child) *Tracklist { func() fyne.CanvasObject { return NewTrackRow(t.colLayout) }, 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. @@ -136,3 +161,18 @@ func (t *Tracklist) onPlayTrackAt(idx int) { t.OnPlayTrackAt(idx) } } + +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) + } else if (d.ActiveKeyModifiers() & fyne.KeyModifierShift) != 0 { + t.SelectionMgr.SelectRange(idx) + } else { + t.SelectionMgr.Select(idx) + } + } else { + t.SelectionMgr.Select(idx) + } + t.list.Refresh() +} From edea337e7ff39aaf73336e1afd24aa3cba42f3e3 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 8 Feb 2023 16:48:38 -0800 Subject: [PATCH 2/9] 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 +} From a4ce33af35af24016a5f2c2c0742e845020c298a Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 8 Feb 2023 16:58:01 -0800 Subject: [PATCH 3/9] some refactoring around saved pages --- ui/browsing/albumpage.go | 61 ++++++++++++++++++------------------- ui/browsing/artistpage.go | 38 +++++++++-------------- ui/browsing/playlistpage.go | 36 +++++++++------------- 3 files changed, 58 insertions(+), 77 deletions(-) diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index e29ff8e..82bb207 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -19,21 +19,26 @@ import ( type AlbumPage struct { widget.BaseWidget - albumID string - sm *backend.ServerManager - pm *backend.PlaybackManager - im *backend.ImageManager - lm *backend.LibraryManager - nav func(Route) - header *AlbumPageHeader - tracklist *widgets.Tracklist - nowPlayingID string - container *fyne.Container - popUpProvider util.PopUpProvider + albumPageState + + header *AlbumPageHeader + tracklist *widgets.Tracklist + nowPlayingID string + container *fyne.Container OnPlayAlbum func(string, int) } +type albumPageState struct { + albumID string + lm *backend.LibraryManager + pm *backend.PlaybackManager + im *backend.ImageManager + sm *backend.ServerManager + popUpProvider util.PopUpProvider + nav func(Route) +} + func NewAlbumPage( albumID string, sm *backend.ServerManager, @@ -43,7 +48,17 @@ func NewAlbumPage( popUpProvider util.PopUpProvider, nav func(Route), ) *AlbumPage { - a := &AlbumPage{albumID: albumID, sm: sm, pm: pm, lm: lm, im: im, nav: nav, popUpProvider: popUpProvider} + a := &AlbumPage{ + albumPageState: albumPageState{ + 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) @@ -72,14 +87,8 @@ func (a *AlbumPage) SetPlayAlbumCallback(cb func(string, int)) { } 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, - } + s := a.albumPageState + return &s } func (a *AlbumPage) Route() Route { @@ -239,16 +248,6 @@ func formatMiscLabelStr(a *subsonic.AlbumID3) string { return fmt.Sprintf("%d · %d tracks · %s", a.Year, a.SongCount, util.SecondsToTimeString(float64(a.Duration))) } -type savedAlbumPage struct { - albumID string - lm *backend.LibraryManager - pm *backend.PlaybackManager - im *backend.ImageManager - sm *backend.ServerManager - popUpProvider util.PopUpProvider - nav func(Route) -} - -func (s *savedAlbumPage) Restore() Page { +func (s *albumPageState) Restore() Page { return NewAlbumPage(s.albumID, s.sm, s.pm, s.lm, s.im, s.popUpProvider, s.nav) } diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 765e3b2..b83d9a8 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -21,16 +21,19 @@ import ( var _ fyne.Widget = (*ArtistPage)(nil) +type artistPageState struct { + artistID string + sm *backend.ServerManager + im *backend.ImageManager + nav func(Route) + popUpProvider util.PopUpProvider +} + type ArtistPage struct { widget.BaseWidget - artistID string - im *backend.ImageManager - sm *backend.ServerManager - nav func(Route) - popUpProvider util.PopUpProvider + artistPageState - grid *widgets.AlbumGrid header *ArtistPageHeader container *fyne.Container @@ -38,13 +41,13 @@ type ArtistPage struct { } func NewArtistPage(artistID string, sm *backend.ServerManager, im *backend.ImageManager, popUp util.PopUpProvider, nav func(Route)) *ArtistPage { - a := &ArtistPage{ + a := &ArtistPage{artistPageState: artistPageState{ artistID: artistID, sm: sm, im: im, nav: nav, popUpProvider: popUp, - } + }} a.ExtendBaseWidget(a) a.header = NewArtistPageHeader(a, nav) a.container = container.NewBorder( @@ -67,13 +70,8 @@ func (a *ArtistPage) Reload() { } func (a *ArtistPage) Save() SavedPage { - return &savedArtistPage{ - artistID: a.artistID, - sm: a.sm, - im: a.im, - nav: a.nav, - popUpProvider: a.popUpProvider, - } + s := a.artistPageState + return &s } func (a *ArtistPage) onPlayAlbum(albumID string) { @@ -112,15 +110,7 @@ func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(a.container) } -type savedArtistPage struct { - artistID string - sm *backend.ServerManager - im *backend.ImageManager - nav func(Route) - popUpProvider util.PopUpProvider -} - -func (s *savedArtistPage) Restore() Page { +func (s *artistPageState) Restore() Page { return NewArtistPage(s.artistID, s.sm, s.im, s.popUpProvider, s.nav) } diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 178ad4a..523b358 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -19,17 +19,22 @@ import ( type PlaylistPage struct { widget.BaseWidget - playlistID string - sm *backend.ServerManager - pm *backend.PlaybackManager - im *backend.ImageManager - nav func(Route) + playlistPageState + header *PlaylistPageHeader tracklist *widgets.Tracklist nowPlayingID string container *fyne.Container } +type playlistPageState struct { + playlistID string + sm *backend.ServerManager + pm *backend.PlaybackManager + im *backend.ImageManager + nav func(Route) +} + func NewPlaylistPage( playlistID string, sm *backend.ServerManager, @@ -37,7 +42,7 @@ func NewPlaylistPage( im *backend.ImageManager, nav func(Route), ) *PlaylistPage { - a := &PlaylistPage{playlistID: playlistID, sm: sm, pm: pm, im: im} + a := &PlaylistPage{playlistPageState: playlistPageState{playlistID: playlistID, sm: sm, pm: pm, im: im}} a.ExtendBaseWidget(a) a.header = NewPlaylistPageHeader(a) a.tracklist = widgets.NewTracklist(nil) @@ -62,13 +67,8 @@ func (a *PlaylistPage) CreateRenderer() fyne.WidgetRenderer { } func (a *PlaylistPage) Save() SavedPage { - return &savedPlaylistPage{ - playlistID: a.playlistID, - sm: a.sm, - pm: a.pm, - im: a.im, - nav: a.nav, - } + p := a.playlistPageState + return &p } func (a *PlaylistPage) Route() Route { @@ -194,14 +194,6 @@ func (a *PlaylistPageHeader) formatPlaylistTrackTimeStr(p *subsonic.Playlist) st return fmt.Sprintf("%d tracks, %s", p.SongCount, util.SecondsToTimeString(float64(p.Duration))) } -type savedPlaylistPage struct { - playlistID string - sm *backend.ServerManager - pm *backend.PlaybackManager - im *backend.ImageManager - nav func(Route) -} - -func (s *savedPlaylistPage) Restore() Page { +func (s *playlistPageState) Restore() Page { return NewPlaylistPage(s.playlistID, s.sm, s.pm, s.im, s.nav) } From a508206264c2c1a7ab3864340f32c2a3e9c7524f Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 8 Feb 2023 17:18:14 -0800 Subject: [PATCH 4/9] refactor album iterator + add fn to get user owned playlsits --- backend/albumiterator.go | 16 ---------------- backend/librarymanager.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 16 deletions(-) create mode 100644 backend/librarymanager.go diff --git a/backend/albumiterator.go b/backend/albumiterator.go index 9b163f3..9f3d2bf 100644 --- a/backend/albumiterator.go +++ b/backend/albumiterator.go @@ -7,22 +7,6 @@ import ( subsonic "github.com/dweymouth/go-subsonic" ) -type AlbumIterator interface { - Next() *subsonic.AlbumID3 -} - -type LibraryManager struct { - PreCacheCoverFn func(string) - - s *ServerManager -} - -func NewLibraryManager(s *ServerManager) *LibraryManager { - return &LibraryManager{ - s: s, - } -} - type AlbumSortOrder string const ( diff --git a/backend/librarymanager.go b/backend/librarymanager.go new file mode 100644 index 0000000..e02e6ed --- /dev/null +++ b/backend/librarymanager.go @@ -0,0 +1,35 @@ +package backend + +import ( + subsonic "github.com/dweymouth/go-subsonic" +) + +type AlbumIterator interface { + Next() *subsonic.AlbumID3 +} + +type LibraryManager struct { + PreCacheCoverFn func(string) + + s *ServerManager +} + +func NewLibraryManager(s *ServerManager) *LibraryManager { + return &LibraryManager{ + s: s, + } +} + +func (l *LibraryManager) GetUserOwnedPlaylists() ([]*subsonic.Playlist, error) { + pl, err := l.s.Server.GetPlaylists(nil) + userPl := make([]*subsonic.Playlist, 0) + if err != nil { + return nil, err + } + for _, p := range pl { + if p.Owner == l.s.Server.User { + userPl = append(userPl, p) + } + } + return userPl, nil +} From 46c87944dbedd5416e8ab1d5685636c8111658c1 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 9 Feb 2023 18:59:11 -0800 Subject: [PATCH 5/9] create addtoplaylistdialog --- ui/dialogs/addtoplaylistdialog.go | 101 ++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 ui/dialogs/addtoplaylistdialog.go diff --git a/ui/dialogs/addtoplaylistdialog.go b/ui/dialogs/addtoplaylistdialog.go new file mode 100644 index 0000000..61bbb27 --- /dev/null +++ b/ui/dialogs/addtoplaylistdialog.go @@ -0,0 +1,101 @@ +package dialogs + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/widget" +) + +type AddToPlaylistDialog struct { + widget.BaseWidget + + OnCanceled func() + OnSubmit func(playlistChoice int, newPlaylistName string) + + playlistSelect *widget.Select + newPlaylistLabel *widget.Label + newPlaylistName *widget.Entry + okBtn *widget.Button + + container *fyne.Container +} + +var _ fyne.Widget = (*AddToPlaylistDialog)(nil) + +func NewAddToPlaylistDialog(title string, existingPlaylistNames []string) *AddToPlaylistDialog { + a := &AddToPlaylistDialog{} + a.ExtendBaseWidget(a) + + titleLabel := widget.NewLabel(title) + titleLabel.TextStyle.Bold = true + options := []string{"New playlist..."} + options = append(options, existingPlaylistNames...) + a.playlistSelect = widget.NewSelect(options, func(_ string) { + if a.playlistSelect.SelectedIndex() == 0 { + a.newPlaylistName.Show() + a.newPlaylistLabel.Show() + if len(a.newPlaylistName.Text) == 0 { + a.okBtn.Disable() + } else { + a.okBtn.Enable() + } + } else { + a.newPlaylistName.Hide() + a.newPlaylistLabel.Hide() + a.okBtn.Enable() + } + }) + a.newPlaylistName = widget.NewEntry() + a.newPlaylistName.Hidden = true + a.newPlaylistName.OnChanged = func(text string) { + if len(text) > 0 { + a.okBtn.Enable() + } + } + a.newPlaylistLabel = widget.NewLabel("Name") + a.newPlaylistLabel.Hidden = true + + a.okBtn = widget.NewButton("OK", a.onOK) + cancelBtn := widget.NewButton("Cancel", a.onCancel) + + a.container = container.NewVBox( + container.NewHBox(layout.NewSpacer(), titleLabel, layout.NewSpacer()), + container.New(layout.NewFormLayout(), + widget.NewLabel("Playlist"), + a.playlistSelect, + a.newPlaylistLabel, + a.newPlaylistName), + widget.NewSeparator(), + container.NewHBox(layout.NewSpacer(), a.okBtn, cancelBtn)) + + return a +} + +func (a *AddToPlaylistDialog) onOK() { + var newPlaylistName string + playlistChoice := -1 + if sel := a.playlistSelect.SelectedIndex(); sel == 0 { + newPlaylistName = a.newPlaylistName.Text + } else { + playlistChoice = sel - 1 + } + if a.OnSubmit != nil { + a.OnSubmit(playlistChoice, newPlaylistName) + } +} + +func (a *AddToPlaylistDialog) onCancel() { + if a.OnCanceled != nil { + a.OnCanceled() + } +} + +func (a *AddToPlaylistDialog) MinSize() fyne.Size { + a.ExtendBaseWidget(a) + return fyne.NewSize(300, a.container.MinSize().Height) +} + +func (a *AddToPlaylistDialog) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(a.container) +} From cd6dce5e3fcf484f65b89e13956a1160260c5d40 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 9 Feb 2023 18:59:49 -0800 Subject: [PATCH 6/9] ui refactoring (create controller pkg) + beginning of add to playlist --- ui/browsing/albumpage.go | 35 ++++++++++---------- ui/browsing/artistpage.go | 27 ++++++++-------- ui/browsing/router.go | 27 ++++------------ ui/controller/controller.go | 64 +++++++++++++++++++++++++++++++++++++ ui/mainwindow.go | 8 ++++- ui/util/util.go | 22 ------------- 6 files changed, 109 insertions(+), 74 deletions(-) create mode 100644 ui/controller/controller.go diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 82bb207..9f23e86 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "supersonic/backend" + "supersonic/ui/controller" "supersonic/ui/layouts" "supersonic/ui/util" "supersonic/ui/widgets" @@ -30,13 +31,13 @@ type AlbumPage struct { } type albumPageState struct { - albumID string - lm *backend.LibraryManager - pm *backend.PlaybackManager - im *backend.ImageManager - sm *backend.ServerManager - popUpProvider util.PopUpProvider - nav func(Route) + albumID string + lm *backend.LibraryManager + pm *backend.PlaybackManager + im *backend.ImageManager + sm *backend.ServerManager + contr *controller.Controller + nav func(Route) } func NewAlbumPage( @@ -45,18 +46,18 @@ func NewAlbumPage( pm *backend.PlaybackManager, lm *backend.LibraryManager, im *backend.ImageManager, - popUpProvider util.PopUpProvider, + contr *controller.Controller, nav func(Route), ) *AlbumPage { a := &AlbumPage{ albumPageState: albumPageState{ - albumID: albumID, - sm: sm, - pm: pm, - lm: lm, - im: im, - nav: nav, - popUpProvider: popUpProvider, + albumID: albumID, + sm: sm, + pm: pm, + lm: lm, + im: im, + nav: nav, + contr: contr, }, } a.ExtendBaseWidget(a) @@ -241,7 +242,7 @@ func (a *AlbumPageHeader) showPopUpCover() { log.Printf("error getting full size album cover: %s", err.Error()) return } - util.ShowPopUpImage(cover, a.page.popUpProvider) + a.page.contr.ShowPopUpImage(cover) } func formatMiscLabelStr(a *subsonic.AlbumID3) string { @@ -249,5 +250,5 @@ func formatMiscLabelStr(a *subsonic.AlbumID3) string { } func (s *albumPageState) Restore() Page { - return NewAlbumPage(s.albumID, s.sm, s.pm, s.lm, s.im, s.popUpProvider, s.nav) + return NewAlbumPage(s.albumID, s.sm, s.pm, s.lm, s.im, s.contr, s.nav) } diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index b83d9a8..36ea5d7 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -7,6 +7,7 @@ import ( "strings" "supersonic/backend" "supersonic/res" + "supersonic/ui/controller" "supersonic/ui/layouts" "supersonic/ui/util" "supersonic/ui/widgets" @@ -22,11 +23,11 @@ import ( var _ fyne.Widget = (*ArtistPage)(nil) type artistPageState struct { - artistID string - sm *backend.ServerManager - im *backend.ImageManager - nav func(Route) - popUpProvider util.PopUpProvider + artistID string + sm *backend.ServerManager + im *backend.ImageManager + nav func(Route) + contr *controller.Controller } type ArtistPage struct { @@ -40,13 +41,13 @@ type ArtistPage struct { OnPlayAlbum func(string, int) } -func NewArtistPage(artistID string, sm *backend.ServerManager, im *backend.ImageManager, popUp util.PopUpProvider, 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, - im: im, - nav: nav, - popUpProvider: popUp, + artistID: artistID, + sm: sm, + im: im, + nav: nav, + contr: contr, }} a.ExtendBaseWidget(a) a.header = NewArtistPageHeader(a, nav) @@ -111,7 +112,7 @@ func (a *ArtistPage) CreateRenderer() fyne.WidgetRenderer { } func (s *artistPageState) Restore() Page { - return NewArtistPage(s.artistID, s.sm, s.im, s.popUpProvider, s.nav) + return NewArtistPage(s.artistID, s.sm, s.im, s.contr, s.nav) } type ArtistPageHeader struct { @@ -190,7 +191,7 @@ func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) { return } a.artistImage.OnTapped = func() { - util.ShowPopUpImage(im, a.artistPage.popUpProvider) + a.artistPage.contr.ShowPopUpImage(im) } a.artistImage.SetImage(im, true /*tappable*/) } diff --git a/ui/browsing/router.go b/ui/browsing/router.go index 17d14d1..3eca1b7 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -2,10 +2,8 @@ package browsing import ( "supersonic/backend" + "supersonic/ui/controller" "supersonic/ui/util" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/widget" ) type PageName int @@ -69,42 +67,29 @@ type NavigationHandler interface { type Router struct { App *backend.App - MainWindow fyne.Window + Controller *controller.Controller Nav NavigationHandler pop util.PopUpProvider } -func NewRouter(app *backend.App, mainWindow fyne.Window, nav NavigationHandler) Router { +func NewRouter(app *backend.App, controller *controller.Controller, nav NavigationHandler) Router { r := Router{ App: app, - MainWindow: mainWindow, + Controller: controller, Nav: nav, } - r.pop = &popUpProvider{window: r.MainWindow} return r } -type popUpProvider struct { - window fyne.Window -} - -func (p *popUpProvider) CreatePopUp(obj fyne.CanvasObject) *widget.PopUp { - return widget.NewPopUp(obj, p.window.Canvas()) -} - -func (p *popUpProvider) WindowSize() fyne.Size { - return p.window.Canvas().Size() -} - func (r Router) CreatePage(rte Route) Page { switch rte.Page { case Album: - return NewAlbumPage(rte.Arg, r.App.ServerManager, r.App.PlaybackManager, 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.Controller, r.OpenRoute) case Albums: return NewAlbumsPage("Albums", rte.Arg, r.App.LibraryManager, r.App.ImageManager, r.OpenRoute) case Artist: - return NewArtistPage(rte.Arg, r.App.ServerManager, r.App.ImageManager, r.pop, r.OpenRoute) + return NewArtistPage(rte.Arg, r.App.ServerManager, r.App.ImageManager, r.Controller, r.OpenRoute) case Artists: return NewArtistsGenresPage(false, r.App.ServerManager, r.OpenRoute) case Favorites: diff --git a/ui/controller/controller.go b/ui/controller/controller.go new file mode 100644 index 0000000..20326ca --- /dev/null +++ b/ui/controller/controller.go @@ -0,0 +1,64 @@ +package controller + +import ( + "image" + "log" + "supersonic/backend" + "supersonic/ui/dialogs" + "supersonic/ui/util" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/widget" + "github.com/dweymouth/go-subsonic" +) + +type Controller struct { + MainWindow fyne.Window + App *backend.App +} + +func (m *Controller) ShowPopUpImage(img image.Image) { + im := canvas.NewImageFromImage(img) + im.FillMode = canvas.ImageFillContain + pop := widget.NewPopUp(im, m.MainWindow.Canvas()) + s := m.MainWindow.Canvas().Size() + var popS fyne.Size + if asp := util.ImageAspect(img); s.Width/s.Height > asp { + // window height is limiting factor + h := s.Height * 0.8 + popS = fyne.NewSize(h*asp, h) + } else { + w := s.Width * 0.8 + popS = fyne.NewSize(w, w*(1/asp)) + } + pop.Resize(popS) + pop.ShowAtPosition(fyne.NewPos( + (s.Width-popS.Width)/2, + (s.Height-popS.Height)/2, + )) +} + +// 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(tracks []*subsonic.Child) { + pls, err := m.App.LibraryManager.GetUserOwnedPlaylists() + if err != nil { + // TODO: surface this error to user + log.Printf("error getting user-owned playlists: %s", err.Error()) + return + } + plNames := make([]string, 0, len(pls)) + for _, pl := range pls { + plNames = append(plNames, pl.Name) + } + + dlg := dialogs.NewAddToPlaylistDialog("Add to Playlist", plNames) + pop := widget.NewModalPopUp(dlg, m.MainWindow.Canvas()) + dlg.OnCanceled = pop.Hide + dlg.OnSubmit = func(playlistChoice int, newPlaylistName string) { + pop.Hide() + // call server to add tracks to playlist + } +} diff --git a/ui/mainwindow.go b/ui/mainwindow.go index 0111a6b..a24b283 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -4,6 +4,7 @@ import ( "supersonic/backend" "supersonic/res" "supersonic/ui/browsing" + "supersonic/ui/controller" "supersonic/ui/os" "supersonic/ui/widgets" @@ -26,6 +27,7 @@ type MainWindow struct { App *backend.App Router browsing.Router + Controller *controller.Controller BrowsingPane *browsing.BrowsingPane BottomPanel *BottomPanel @@ -43,7 +45,11 @@ func NewMainWindow(fyneApp fyne.App, appName string, app *backend.App, size fyne BrowsingPane: browsing.NewBrowsingPane(app), } - m.Router = browsing.NewRouter(app, m.Window, m.BrowsingPane) + m.Controller = &controller.Controller{ + MainWindow: m.Window, + App: app, + } + m.Router = browsing.NewRouter(app, m.Controller, m.BrowsingPane) m.BottomPanel = NewBottomPanel(app.Player, m.Router.OpenRoute) m.BottomPanel.SetPlaybackManager(app.PlaybackManager) m.BottomPanel.ImageManager = app.ImageManager diff --git a/ui/util/util.go b/ui/util/util.go index dee6e3c..edce626 100644 --- a/ui/util/util.go +++ b/ui/util/util.go @@ -7,7 +7,6 @@ import ( "strings" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/widget" "golang.org/x/net/html" ) @@ -33,27 +32,6 @@ type PopUpProvider interface { WindowSize() fyne.Size } -func ShowPopUpImage(img image.Image, popUpProvider PopUpProvider) { - im := canvas.NewImageFromImage(img) - im.FillMode = canvas.ImageFillContain - pop := popUpProvider.CreatePopUp(im) - s := popUpProvider.WindowSize() - var popS fyne.Size - if asp := ImageAspect(img); s.Width/s.Height > asp { - // window height is limiting factor - h := s.Height * 0.8 - popS = fyne.NewSize(h*asp, h) - } else { - w := s.Width * 0.8 - popS = fyne.NewSize(w, w*(1/asp)) - } - pop.Resize(popS) - pop.ShowAtPosition(fyne.NewPos( - (s.Width-popS.Width)/2, - (s.Height-popS.Height)/2, - )) -} - func RichTextSegsFromHTMLString(s string) []widget.RichTextSegment { tokr := html.NewTokenizer(strings.NewReader(s)) var segs []widget.RichTextSegment From c66c43ecc804128e2d448789666e7e309cb754be Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 9 Feb 2023 20:14:26 -0800 Subject: [PATCH 7/9] update subsonic client library --- backend/albumiterator.go | 2 +- backend/librarymanager.go | 2 +- backend/playbackmanager.go | 2 +- backend/servermanager.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- ui/bottompanel.go | 2 +- ui/browsing/albumpage.go | 2 +- ui/browsing/artistpage.go | 2 +- ui/browsing/artistsgenrespage.go | 2 +- ui/browsing/browsingpane.go | 2 +- ui/browsing/favoritespage.go | 2 +- ui/browsing/genrepage.go | 2 +- ui/browsing/playlistpage.go | 2 +- ui/browsing/playlistspage.go | 2 +- ui/controller/controller.go | 2 +- ui/mainwindow.go | 2 +- ui/widgets/albumcard.go | 2 +- ui/widgets/albumgrid.go | 2 +- ui/widgets/tracklist.go | 2 +- 20 files changed, 21 insertions(+), 21 deletions(-) diff --git a/backend/albumiterator.go b/backend/albumiterator.go index 9f3d2bf..8bb12c8 100644 --- a/backend/albumiterator.go +++ b/backend/albumiterator.go @@ -4,7 +4,7 @@ import ( "log" "strconv" - subsonic "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" ) type AlbumSortOrder string diff --git a/backend/librarymanager.go b/backend/librarymanager.go index e02e6ed..8ad7801 100644 --- a/backend/librarymanager.go +++ b/backend/librarymanager.go @@ -1,7 +1,7 @@ package backend import ( - subsonic "github.com/dweymouth/go-subsonic" + subsonic "github.com/dweymouth/go-subsonic/subsonic" ) type AlbumIterator interface { diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 70feab3..94ea457 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -8,7 +8,7 @@ import ( "supersonic/player" "time" - subsonic "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" ) const ( diff --git a/backend/servermanager.go b/backend/servermanager.go index a973fa9..f71f7e0 100644 --- a/backend/servermanager.go +++ b/backend/servermanager.go @@ -3,7 +3,7 @@ package backend import ( "net/http" - subsonic "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" "github.com/google/uuid" ) diff --git a/go.mod b/go.mod index 86d6d4f..2aa4e7e 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( fyne.io/fyne/v2 v2.2.4 github.com/20after4/configdir v0.1.1 - github.com/dweymouth/go-subsonic v0.0.0-20221214005741-bd8048fa1863 + github.com/dweymouth/go-subsonic v0.0.0-20230210015330-4d630f19878e github.com/google/uuid v1.3.0 github.com/pelletier/go-toml v1.9.3 github.com/wildeyedskies/go-mpv v0.0.0-20221204042335-e8961dc66756 diff --git a/go.sum b/go.sum index e97a078..3949cab 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dweymouth/fyne/v2 v2.2.5-0.20230207011038-d2ef06e09a93 h1:4GGNszUwZhcneapbaBO+rbHtmCjKjZXHKPCqFXQPg4E= github.com/dweymouth/fyne/v2 v2.2.5-0.20230207011038-d2ef06e09a93/go.mod h1:MBoGuHzLLSXdQOWFAwWhIhYTEMp33zqtGCReSWhaQTA= -github.com/dweymouth/go-subsonic v0.0.0-20221214005741-bd8048fa1863 h1:bOWMpFJ9zY839T1EngQ5nxVCiMlatPtpGkg/yHK6szg= -github.com/dweymouth/go-subsonic v0.0.0-20221214005741-bd8048fa1863/go.mod h1:fUez6NFiEJiQTZizZ1BThZr5GJXAbigzGYjEPNm4tdI= +github.com/dweymouth/go-subsonic v0.0.0-20230210015330-4d630f19878e h1:+YR9JrwkWxniJ4/TgvSZze2sSC/USdLswAqebmIK3zo= +github.com/dweymouth/go-subsonic v0.0.0-20230210015330-4d630f19878e/go.mod h1:fUez6NFiEJiQTZizZ1BThZr5GJXAbigzGYjEPNm4tdI= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/ui/bottompanel.go b/ui/bottompanel.go index f1f430d..bf00fb5 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -13,7 +13,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" ) type BottomPanel struct { diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 9f23e86..315da1f 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -14,7 +14,7 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" ) type AlbumPage struct { diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index 36ea5d7..dc1da27 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -17,7 +17,7 @@ import ( "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" ) var _ fyne.Widget = (*ArtistPage)(nil) diff --git a/ui/browsing/artistsgenrespage.go b/ui/browsing/artistsgenrespage.go index c4346eb..4ce0b56 100644 --- a/ui/browsing/artistsgenrespage.go +++ b/ui/browsing/artistsgenrespage.go @@ -11,7 +11,7 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" ) var _ fyne.Widget = (*ArtistPage)(nil) diff --git a/ui/browsing/browsingpane.go b/ui/browsing/browsingpane.go index 55a3638..4efed1c 100644 --- a/ui/browsing/browsingpane.go +++ b/ui/browsing/browsingpane.go @@ -11,7 +11,7 @@ import ( "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" ) type Page interface { diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index c5bef14..82e2b3f 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -10,7 +10,7 @@ import ( "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" ) type FavoritesPage struct { diff --git a/ui/browsing/genrepage.go b/ui/browsing/genrepage.go index c860e1c..5d68e78 100644 --- a/ui/browsing/genrepage.go +++ b/ui/browsing/genrepage.go @@ -9,7 +9,7 @@ import ( "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" ) // TODO: there is a lot of code duplication between this and albumspage. Refactor? diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 523b358..159b01a 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -13,7 +13,7 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" ) type PlaylistPage struct { diff --git a/ui/browsing/playlistspage.go b/ui/browsing/playlistspage.go index 581bf82..95af201 100644 --- a/ui/browsing/playlistspage.go +++ b/ui/browsing/playlistspage.go @@ -11,7 +11,7 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" ) type PlaylistsPage struct { diff --git a/ui/controller/controller.go b/ui/controller/controller.go index 20326ca..857fd1c 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -10,7 +10,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" ) type Controller struct { diff --git a/ui/mainwindow.go b/ui/mainwindow.go index a24b283..399ad14 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -12,7 +12,7 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" ) var ( diff --git a/ui/widgets/albumcard.go b/ui/widgets/albumcard.go index d869159..00ad8a4 100644 --- a/ui/widgets/albumcard.go +++ b/ui/widgets/albumcard.go @@ -14,7 +14,7 @@ import ( "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" ) var _ fyne.Widget = (*AlbumCard)(nil) diff --git a/ui/widgets/albumgrid.go b/ui/widgets/albumgrid.go index 194692b..2c48684 100644 --- a/ui/widgets/albumgrid.go +++ b/ui/widgets/albumgrid.go @@ -8,7 +8,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" ) const albumFetchBatchSize = 6 diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 654c7aa..16dcd03 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -13,7 +13,7 @@ import ( "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic" + "github.com/dweymouth/go-subsonic/subsonic" ) type TrackRow struct { From 12cc315e1285da07ab863a7c7bb6f874ba2f7bdb Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 9 Feb 2023 20:57:42 -0800 Subject: [PATCH 8/9] add to playlist works --- go.mod | 2 +- go.sum | 4 ++-- ui/browsing/albumpage.go | 1 + ui/browsing/playlistpage.go | 8 ++++++-- ui/browsing/router.go | 2 +- ui/controller/controller.go | 12 ++++++++--- ui/dialogs/addtoplaylistdialog.go | 34 +++++++++++++++++++------------ ui/widgets/tracklist.go | 17 ++++++++++++++-- 8 files changed, 56 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 2aa4e7e..40b32ff 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( fyne.io/fyne/v2 v2.2.4 github.com/20after4/configdir v0.1.1 - github.com/dweymouth/go-subsonic v0.0.0-20230210015330-4d630f19878e + github.com/dweymouth/go-subsonic v0.0.0-20230210044542-537b9238299b github.com/google/uuid v1.3.0 github.com/pelletier/go-toml v1.9.3 github.com/wildeyedskies/go-mpv v0.0.0-20221204042335-e8961dc66756 diff --git a/go.sum b/go.sum index 3949cab..ec8c5d3 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dweymouth/fyne/v2 v2.2.5-0.20230207011038-d2ef06e09a93 h1:4GGNszUwZhcneapbaBO+rbHtmCjKjZXHKPCqFXQPg4E= github.com/dweymouth/fyne/v2 v2.2.5-0.20230207011038-d2ef06e09a93/go.mod h1:MBoGuHzLLSXdQOWFAwWhIhYTEMp33zqtGCReSWhaQTA= -github.com/dweymouth/go-subsonic v0.0.0-20230210015330-4d630f19878e h1:+YR9JrwkWxniJ4/TgvSZze2sSC/USdLswAqebmIK3zo= -github.com/dweymouth/go-subsonic v0.0.0-20230210015330-4d630f19878e/go.mod h1:fUez6NFiEJiQTZizZ1BThZr5GJXAbigzGYjEPNm4tdI= +github.com/dweymouth/go-subsonic v0.0.0-20230210044542-537b9238299b h1:8JbTKYDdQg6JKu7ZDbEaVbXxutc3pRF58yNvTVMBHec= +github.com/dweymouth/go-subsonic v0.0.0-20230210044542-537b9238299b/go.mod h1:fUez6NFiEJiQTZizZ1BThZr5GJXAbigzGYjEPNm4tdI= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 315da1f..7e90870 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -70,6 +70,7 @@ func NewAlbumPage( a.pm.LoadTracks(tracks, false) a.pm.PlayFromBeginning() } + a.tracklist.OnAddToPlaylist = a.contr.DoAddTracksToPlaylistWorkflow a.container = container.NewBorder( container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 15, PadBottom: 10}, a.header), diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 159b01a..cc4e799 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -5,6 +5,7 @@ import ( "log" "supersonic/backend" "supersonic/res" + "supersonic/ui/controller" "supersonic/ui/layouts" "supersonic/ui/util" "supersonic/ui/widgets" @@ -29,6 +30,7 @@ type PlaylistPage struct { type playlistPageState struct { playlistID string + contr *controller.Controller sm *backend.ServerManager pm *backend.PlaybackManager im *backend.ImageManager @@ -37,12 +39,13 @@ type playlistPageState struct { func NewPlaylistPage( playlistID string, + contr *controller.Controller, sm *backend.ServerManager, pm *backend.PlaybackManager, im *backend.ImageManager, nav func(Route), ) *PlaylistPage { - a := &PlaylistPage{playlistPageState: playlistPageState{playlistID: playlistID, sm: sm, pm: pm, im: im}} + a := &PlaylistPage{playlistPageState: playlistPageState{playlistID: playlistID, contr: contr, sm: sm, pm: pm, im: im}} a.ExtendBaseWidget(a) a.header = NewPlaylistPageHeader(a) a.tracklist = widgets.NewTracklist(nil) @@ -54,6 +57,7 @@ func NewPlaylistPage( a.pm.LoadTracks(tracks, false) a.pm.PlayFromBeginning() } + a.tracklist.OnAddToPlaylist = a.contr.DoAddTracksToPlaylistWorkflow a.container = container.NewBorder( container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15, PadTop: 15, PadBottom: 10}, a.header), @@ -195,5 +199,5 @@ func (a *PlaylistPageHeader) formatPlaylistTrackTimeStr(p *subsonic.Playlist) st } func (s *playlistPageState) Restore() Page { - return NewPlaylistPage(s.playlistID, s.sm, s.pm, s.im, s.nav) + return NewPlaylistPage(s.playlistID, s.contr, s.sm, s.pm, s.im, s.nav) } diff --git a/ui/browsing/router.go b/ui/browsing/router.go index 3eca1b7..32ac87b 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -99,7 +99,7 @@ func (r Router) CreatePage(rte Route) Page { case Genres: return NewArtistsGenresPage(true, r.App.ServerManager, r.OpenRoute) case Playlist: - return NewPlaylistPage(rte.Arg, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager, r.OpenRoute) + return NewPlaylistPage(rte.Arg, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager, r.OpenRoute) case Playlists: return NewPlaylistsPage(r.App.ServerManager, r.OpenRoute) } diff --git a/ui/controller/controller.go b/ui/controller/controller.go index 857fd1c..bed132a 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -10,7 +10,6 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/go-subsonic/subsonic" ) type Controller struct { @@ -42,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(tracks []*subsonic.Child) { +func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { pls, err := m.App.LibraryManager.GetUserOwnedPlaylists() if err != nil { // TODO: surface this error to user @@ -59,6 +58,13 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(tracks []*subsonic.Child) { dlg.OnCanceled = pop.Hide dlg.OnSubmit = func(playlistChoice int, newPlaylistName string) { pop.Hide() - // call server to add tracks to playlist + if playlistChoice < 0 { + m.App.ServerManager.Server.CreatePlaylistWithTracks( + trackIDs, map[string]string{"name": newPlaylistName}) + } else { + m.App.ServerManager.Server.UpdatePlaylistTracks( + pls[playlistChoice].ID, trackIDs, nil /*tracksToRemove*/) + } } + pop.Show() } diff --git a/ui/dialogs/addtoplaylistdialog.go b/ui/dialogs/addtoplaylistdialog.go index 61bbb27..11e235c 100644 --- a/ui/dialogs/addtoplaylistdialog.go +++ b/ui/dialogs/addtoplaylistdialog.go @@ -32,31 +32,23 @@ func NewAddToPlaylistDialog(title string, existingPlaylistNames []string) *AddTo options := []string{"New playlist..."} options = append(options, existingPlaylistNames...) a.playlistSelect = widget.NewSelect(options, func(_ string) { - if a.playlistSelect.SelectedIndex() == 0 { - a.newPlaylistName.Show() - a.newPlaylistLabel.Show() - if len(a.newPlaylistName.Text) == 0 { - a.okBtn.Disable() - } else { - a.okBtn.Enable() - } - } else { - a.newPlaylistName.Hide() - a.newPlaylistLabel.Hide() - a.okBtn.Enable() - } + a.onSelectionChanged() }) + a.playlistSelect.PlaceHolder = "(Choose playlist)" a.newPlaylistName = widget.NewEntry() a.newPlaylistName.Hidden = true a.newPlaylistName.OnChanged = func(text string) { if len(text) > 0 { a.okBtn.Enable() + } else { + a.okBtn.Disable() } } a.newPlaylistLabel = widget.NewLabel("Name") a.newPlaylistLabel.Hidden = true a.okBtn = widget.NewButton("OK", a.onOK) + a.okBtn.Disable() cancelBtn := widget.NewButton("Cancel", a.onCancel) a.container = container.NewVBox( @@ -85,6 +77,22 @@ func (a *AddToPlaylistDialog) onOK() { } } +func (a *AddToPlaylistDialog) onSelectionChanged() { + if a.playlistSelect.SelectedIndex() == 0 { + a.newPlaylistName.Show() + a.newPlaylistLabel.Show() + if len(a.newPlaylistName.Text) == 0 { + a.okBtn.Disable() + } else { + a.okBtn.Enable() + } + } else { + a.newPlaylistName.Hide() + a.newPlaylistLabel.Hide() + a.okBtn.Enable() + } +} + func (a *AddToPlaylistDialog) onCancel() { if a.OnCanceled != nil { a.OnCanceled() diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 16dcd03..11cfd78 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -117,7 +117,7 @@ type Tracklist struct { OnPlayTrackAt func(int) OnPlaySelection func(tracks []*subsonic.Child) OnAddToQueue func(trackIDs []*subsonic.Child) - OnAddToPlaylist func(trackIDs []*subsonic.Child) + OnAddToPlaylist func(trackIDs []string) selectionMgr util.ListSelectionManager nowPlayingIdx int @@ -213,7 +213,11 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) { t.OnAddToQueue(t.selectedTracks()) } }), - //fyne.NewMenuItem("Add to playlist...", func() {}), + fyne.NewMenuItem("Add to playlist...", func() { + if t.OnAddToPlaylist != nil { + t.OnAddToPlaylist(t.selectedTrackIDs()) + } + }), ) } widget.ShowPopUpMenuAtPosition(t.ctxMenu, fyne.CurrentApp().Driver().CanvasForObject(t), e.AbsolutePosition) @@ -227,3 +231,12 @@ func (t *Tracklist) selectedTracks() []*subsonic.Child { } return tracks } + +func (t *Tracklist) selectedTrackIDs() []string { + sel := t.selectionMgr.GetSelection() + tracks := make([]string, 0, len(sel)) + for _, idx := range sel { + tracks = append(tracks, t.Tracks[idx].ID) + } + return tracks +} From dca82b8fee01ac4cec849728ba9e6ea520d39903 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 10 Feb 2023 17:17:42 -0800 Subject: [PATCH 9/9] add remove from playlist support --- ui/browsing/playlistpage.go | 8 ++++++++ ui/widgets/tracklist.go | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index cc4e799..fee6f9c 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -50,6 +50,9 @@ func NewPlaylistPage( a.header = NewPlaylistPageHeader(a) a.tracklist = widgets.NewTracklist(nil) a.tracklist.AutoNumber = true + a.tracklist.AuxiliaryMenuItems = []*fyne.MenuItem{ + fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist), + } // connect tracklist actions a.tracklist.OnPlayTrackAt = a.onPlayTrackAt a.tracklist.OnAddToQueue = func(tracks []*subsonic.Child) { a.pm.LoadTracks(tracks, true) } @@ -114,6 +117,11 @@ func (a *PlaylistPage) loadAsync() { }() } +func (a *PlaylistPage) onRemoveSelectedFromPlaylist() { + a.sm.Server.UpdatePlaylistTracks(a.playlistID, nil, a.tracklist.SelectedTrackIndexes()) + go a.Reload() +} + type PlaylistPageHeader struct { widget.BaseWidget diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 11cfd78..32fde47 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -112,6 +112,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 // user action callbacks OnPlayTrackAt func(int) @@ -219,6 +221,10 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) { } }), ) + if len(t.AuxiliaryMenuItems) > 0 { + t.ctxMenu.Items = append(t.ctxMenu.Items, fyne.NewMenuItemSeparator()) + t.ctxMenu.Items = append(t.ctxMenu.Items, t.AuxiliaryMenuItems...) + } } widget.ShowPopUpMenuAtPosition(t.ctxMenu, fyne.CurrentApp().Driver().CanvasForObject(t), e.AbsolutePosition) } @@ -240,3 +246,7 @@ func (t *Tracklist) selectedTrackIDs() []string { } return tracks } + +func (t *Tracklist) SelectedTrackIndexes() []int { + return t.selectionMgr.GetSelection() +}