diff --git a/go.mod b/go.mod index ec9317f2..ddb1f39e 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 github.com/cenkalti/dominantcolor v1.0.2 github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 + github.com/dweymouth/fyne-advanced-list v0.0.0-20240622222843-deee68d6c703 github.com/dweymouth/fyne-lyrics v0.0.0-20240528234907-15eee7ce5e64 github.com/dweymouth/go-jellyfin v0.0.0-20240517151952-5ceca61cb645 github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee @@ -28,7 +29,6 @@ require ( github.com/danieljoos/wincred v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/disintegration/imaging v1.6.2 // indirect - github.com/dweymouth/fyne-advanced-list v0.0.0-20240614152514-d7bef361f680 // indirect github.com/fredbi/uri v1.1.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect diff --git a/go.sum b/go.sum index 192940cb..3643f6c5 100644 --- a/go.sum +++ b/go.sum @@ -84,10 +84,8 @@ github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 h1:mGvOb3zxl4vCLv+ github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1/go.mod h1:ZNCLJfehvEf34B7BbLKjgpsL9lyW7q938w/GY1XgV4E= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= -github.com/dweymouth/fyne-advanced-list v0.0.0-20240614151622-9f11c64def19 h1:AfKaUmPlcXyQRlCH/3tX2A+oIOKLC14Ns3AfIjn4NC4= -github.com/dweymouth/fyne-advanced-list v0.0.0-20240614151622-9f11c64def19/go.mod h1:5bICtCEhLzJ4MlRORlUa3L0CadB6xQTNW9DhTXl/UN0= -github.com/dweymouth/fyne-advanced-list v0.0.0-20240614152514-d7bef361f680 h1:PDffxh0kv4czCCrFV2uSWDRKnIgpaDZhqohB4mYX9Vg= -github.com/dweymouth/fyne-advanced-list v0.0.0-20240614152514-d7bef361f680/go.mod h1:5bICtCEhLzJ4MlRORlUa3L0CadB6xQTNW9DhTXl/UN0= +github.com/dweymouth/fyne-advanced-list v0.0.0-20240622222843-deee68d6c703 h1:SHIBiCpGHdhJfVApZ0H/+hxu0/pg8YjnSP082rNoI0A= +github.com/dweymouth/fyne-advanced-list v0.0.0-20240622222843-deee68d6c703/go.mod h1:sbOhla4VcfFb4OjXiUFTLXMPTnhRUlVrDMhB8HtWR4o= github.com/dweymouth/fyne-lyrics v0.0.0-20240528234907-15eee7ce5e64 h1:RUIrnGY034rDMlcOui/daurwX5b+52KdUKhH9aXaDSg= github.com/dweymouth/fyne-lyrics v0.0.0-20240528234907-15eee7ce5e64/go.mod h1:3YrjFDHMlhCsSZ/OvmJCxWm9QHSgOVWZBxnraZz9Z7c= github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20240604143614-256525c6a602 h1:k3jFLjmAuPJ5ZFNF57szZp8XrLIb6mIdEEGPkm6EZ7Q= diff --git a/sharedutil/sharedutil.go b/sharedutil/sharedutil.go index bad0e27a..cf5bccb5 100644 --- a/sharedutil/sharedutil.go +++ b/sharedutil/sharedutil.go @@ -1,9 +1,6 @@ package sharedutil import ( - "math" - "slices" - "github.com/dweymouth/supersonic/backend/mediaprovider" ) @@ -106,90 +103,33 @@ func TracksToIDs(tracks []*mediaprovider.Track) []string { }) } -type TrackReorderOp int - -const ( - MoveToTop TrackReorderOp = iota - MoveToBottom - MoveUp - MoveDown -) - // Reorder items and return a new track slice. // idxToMove must contain only valid indexes into tracks, and no repeats -func ReorderItems[T any](items []T, idxToMove []int, op TrackReorderOp) []T { - newItems := make([]T, len(items)) - switch op { - case MoveToTop: - topIdx := 0 - botIdx := len(idxToMove) - idxToMoveSet := ToSet(idxToMove) - for i, t := range items { - if _, ok := idxToMoveSet[i]; ok { - newItems[topIdx] = t - topIdx++ - } else { - newItems[botIdx] = t - botIdx++ - } - } - case MoveToBottom: - topIdx := 0 - botIdx := len(items) - len(idxToMove) - idxToMoveSet := ToSet(idxToMove) - for i, t := range items { - if _, ok := idxToMoveSet[i]; ok { - newItems[botIdx] = t - botIdx++ - } else { - newItems[topIdx] = t - topIdx++ - } - } - case MoveUp: - first := firstIdxCanMoveUp(idxToMove) - copy(newItems, items) - for _, i := range idxToMove { - if i < first { - continue - } - newItems[i-1], newItems[i] = newItems[i], newItems[i-1] +func ReorderItems[T any](items []T, idxToMove []int, insertIdx int) []T { + idxToMoveSet := ToSet(idxToMove) + + newItems := make([]T, 0, len(items)) + + // collect items that will end up before the insertion set + i := 0 + for ; i < len(items); i++ { + if insertIdx == i { + break } - case MoveDown: - last := lastIdxCanMoveDown(idxToMove, len(items)) - copy(newItems, items) - for i := len(idxToMove) - 1; i >= 0; i-- { - idx := idxToMove[i] - if idx > last { - continue - } - newItems[idx+1], newItems[idx] = newItems[idx], newItems[idx+1] + if _, ok := idxToMoveSet[i]; !ok { + newItems = append(newItems, items[i]) } } - return newItems -} -func firstIdxCanMoveUp(idxs []int) int { - prevIdx := -1 - slices.Sort(idxs) - for _, idx := range idxs { - if idx > prevIdx+1 { - return idx - } - prevIdx = idx + for _, idx := range idxToMove { + newItems = append(newItems, items[idx]) } - return math.MaxInt -} -func lastIdxCanMoveDown(idxs []int, lenSlice int) int { - prevIdx := lenSlice - slices.Sort(idxs) - for i := len(idxs) - 1; i >= 0; i-- { - idx := idxs[i] - if idx < prevIdx-1 { - return idx + for ; i < len(items); i++ { + if _, ok := idxToMoveSet[i]; !ok { + newItems = append(newItems, items[i]) } - prevIdx = idx } - return -1 + + return newItems } diff --git a/sharedutil/sharedutil_test.go b/sharedutil/sharedutil_test.go index 4d395e4d..f4d6e184 100644 --- a/sharedutil/sharedutil_test.go +++ b/sharedutil/sharedutil_test.go @@ -8,6 +8,7 @@ import ( ) func Test_ReorderItems(t *testing.T) { + tracks := []*mediaprovider.Track{ {ID: "a"}, // 0 {ID: "b"}, // 1 @@ -27,7 +28,7 @@ func Test_ReorderItems(t *testing.T) { {ID: "b"}, {ID: "e"}, } - newTracks := ReorderItems(tracks, idxToMove, MoveToTop) + newTracks := ReorderItems(tracks, idxToMove, 0) if !tracklistsEqual(t, newTracks, want) { t.Error("ReorderTracks: MoveToTop order incorrect") } @@ -42,40 +43,10 @@ func Test_ReorderItems(t *testing.T) { {ID: "c"}, {ID: "f"}, } - newTracks = ReorderItems(tracks, idxToMove, MoveToBottom) + newTracks = ReorderItems(tracks, idxToMove, len(tracks)) if !tracklistsEqual(t, newTracks, want) { t.Error("ReorderTracks: MoveToBottom order incorrect") } - - // test MoveUp: - idxToMove = []int{0, 1, 3, 5} - want = []*mediaprovider.Track{ - {ID: "a"}, - {ID: "b"}, - {ID: "d"}, - {ID: "c"}, - {ID: "f"}, - {ID: "e"}, - } - newTracks = ReorderItems(tracks, idxToMove, MoveUp) - if !tracklistsEqual(t, newTracks, want) { - t.Error("ReorderTracks: MoveUp order incorrect") - } - - // test MoveDown: - idxToMove = []int{2, 4, 5} - want = []*mediaprovider.Track{ - {ID: "a"}, - {ID: "b"}, - {ID: "d"}, - {ID: "c"}, - {ID: "e"}, - {ID: "f"}, - } - newTracks = ReorderItems(tracks, idxToMove, MoveDown) - if !tracklistsEqual(t, newTracks, want) { - t.Error("ReorderTracks: MoveDown order incorrect") - } } func tracklistsEqual(t *testing.T, a, b []*mediaprovider.Track) bool { diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index 1f12ec56..cf7ebe38 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -129,6 +129,7 @@ func NewNowPlayingPage( a.queueList = widgets.NewPlayQueueList(a.im, false) a.relatedList = widgets.NewPlayQueueList(a.im, true) + a.queueList.Reorderable = true a.queueList.OnReorderItems = a.doSetNewTrackOrder a.queueList.OnDownload = contr.ShowDownloadDialog a.queueList.OnShare = func(tracks []*mediaprovider.Track) { @@ -502,7 +503,7 @@ func (a *NowPlayingPage) Refresh() { a.BaseWidget.Refresh() } -func (a *NowPlayingPage) doSetNewTrackOrder(trackIDs []string, op sharedutil.TrackReorderOp) { +func (a *NowPlayingPage) doSetNewTrackOrder(trackIDs []string, insertPos int) { trackIDSet := sharedutil.ToSet(trackIDs) idxs := make([]int, 0, len(trackIDs)) for i, tr := range a.queue { @@ -510,7 +511,7 @@ func (a *NowPlayingPage) doSetNewTrackOrder(trackIDs []string, op sharedutil.Tra idxs = append(idxs, i) } } - newTracks := sharedutil.ReorderItems(a.queue, idxs, op) + newTracks := sharedutil.ReorderItems(a.queue, idxs, insertPos) a.pm.UpdatePlayQueue(newTracks) } diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 4eb9f4b7..11cd2770 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -86,17 +86,16 @@ func newPlaylistPage( a.tracklist.OnVisibleColumnsChanged = func(cols []string) { conf.TracklistColumns = cols } + a.tracklist.OnReorderTracks = a.doSetNewTrackOrder _, canRate := a.sm.Server.(mediaprovider.SupportsRating) _, canShare := a.sm.Server.(mediaprovider.SupportsSharing) remove := fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist) remove.Icon = theme.ContentClearIcon() a.tracklist.Options = widgets.TracklistOptions{ - DisableRating: !canRate, - DisableSharing: !canShare, - AuxiliaryMenuItems: []*fyne.MenuItem{ - util.NewReorderTracksSubmenu(a.doSetNewTrackOrder), - remove, - }, + Reorderable: true, + DisableRating: !canRate, + DisableSharing: !canShare, + AuxiliaryMenuItems: []*fyne.MenuItem{remove}, } // connect tracklist actions a.contr.ConnectTracklistActions(a.tracklist) @@ -118,6 +117,7 @@ func (a *PlaylistPage) Save() SavedPage { p.trackSort = a.tracklist.Sorting() p.widgetPool.Release(util.WidgetTypePlaylistPageHeader, a.header) a.tracklist.Clear() + a.tracklist.OnReorderTracks = nil p.widgetPool.Release(util.WidgetTypeTracklist, a.tracklist) return &p } @@ -178,19 +178,19 @@ func renumberTracks(tracks []*mediaprovider.Track) { } } -func (a *PlaylistPage) doSetNewTrackOrder(op sharedutil.TrackReorderOp) { +func (a *PlaylistPage) doSetNewTrackOrder(ids []string, newPos int) { // Since the tracklist view may be sorted in a different order than the // actual running order, we need to get the IDs of the selected tracks // from the tracklist and convert them to indices in the *original* run order - idSet := sharedutil.ToSet(a.tracklist.SelectedTrackIDs()) + idSet := sharedutil.ToSet(ids) idxs := make([]int, 0, len(idSet)) for i, tr := range a.tracks { if _, ok := idSet[tr.ID]; ok { idxs = append(idxs, i) } } - newTracks := sharedutil.ReorderItems(a.tracks, idxs, op) - ids := sharedutil.TracksToIDs(newTracks) + newTracks := sharedutil.ReorderItems(a.tracks, idxs, newPos) + ids = sharedutil.TracksToIDs(newTracks) if err := a.sm.Server.ReplacePlaylistTracks(a.playlistID, ids); err != nil { log.Printf("error updating playlist: %s", err.Error()) } else { diff --git a/ui/util/util.go b/ui/util/util.go index d237ac59..884e715f 100644 --- a/ui/util/util.go +++ b/ui/util/util.go @@ -17,7 +17,6 @@ import ( "fyne.io/fyne/v2/widget" "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/res" - "github.com/dweymouth/supersonic/sharedutil" myTheme "github.com/dweymouth/supersonic/ui/theme" "golang.org/x/net/html" ) @@ -227,18 +226,6 @@ func NewRatingSubmenu(onSetRating func(int)) *fyne.MenuItem { return ratingMenu } -func NewReorderTracksSubmenu(onReorderTracks func(sharedutil.TrackReorderOp)) *fyne.MenuItem { - reorderMenu := fyne.NewMenuItem("Reorder tracks", nil) - reorderMenu.Icon = myTheme.SortIcon - reorderMenu.ChildMenu = fyne.NewMenu("", []*fyne.MenuItem{ - fyne.NewMenuItem("Move to top", func() { onReorderTracks(sharedutil.MoveToTop) }), - fyne.NewMenuItem("Move up", func() { onReorderTracks(sharedutil.MoveUp) }), - fyne.NewMenuItem("Move down", func() { onReorderTracks(sharedutil.MoveDown) }), - fyne.NewMenuItem("Move to bottom", func() { onReorderTracks(sharedutil.MoveToBottom) }), - }...) - return reorderMenu -} - func AddHeaderBackground(obj fyne.CanvasObject) *fyne.Container { return AddHeaderBackgroundWithColorName(obj, myTheme.ColorNamePageHeader) } diff --git a/ui/widgets/playqueuelist.go b/ui/widgets/playqueuelist.go index 71118577..e3e0c244 100644 --- a/ui/widgets/playqueuelist.go +++ b/ui/widgets/playqueuelist.go @@ -31,6 +31,7 @@ type PlayQueueListModel struct { type PlayQueueList struct { widget.BaseWidget + Reorderable bool DisableRating bool DisableSharing bool @@ -46,7 +47,7 @@ type PlayQueueList struct { OnDownload func(tracks []*mediaprovider.Track, downloadName string) OnShare func(tracks []*mediaprovider.Track) OnShowArtistPage func(artistID string) - OnReorderItems func(itemIDs []string, op sharedutil.TrackReorderOp) + OnReorderItems func(itemIDs []string, reorderTo int) useNonQueueMenu bool menu *widget.PopUpMenu // ctx menu for when only tracks are selected @@ -102,6 +103,17 @@ func NewPlayQueueList(im *backend.ImageManager, useNonQueueMenu bool) *PlayQueue tr.Update(model, itemID+1) }, ) + p.list.OnDragBegin = func(id int) { + if !p.items[id].Selected { + p.selectTrack(id) + p.list.Refresh() + } + } + p.list.OnDragEnd = func(dragged, insertPos int) { + if p.OnReorderItems != nil { + p.OnReorderItems(p.selectedItemIDs(), insertPos) + } + } return p } @@ -149,7 +161,12 @@ func (p *PlayQueueList) UnselectAll() { p.tracksMutex.RLock() util.UnselectAllItems(p.items) p.tracksMutex.RUnlock() - p.Refresh() + p.list.Refresh() +} + +func (p *PlayQueueList) Refresh() { + p.list.EnableDragging = p.Reorderable + p.BaseWidget.Refresh() } func (p *PlayQueueList) lenTracks() int { @@ -292,15 +309,9 @@ func (p *PlayQueueList) ensureTracksMenu() { } }) remove.Icon = theme.ContentRemoveIcon() - reorder := util.NewReorderTracksSubmenu(func(tro sharedutil.TrackReorderOp) { - if p.OnReorderItems != nil { - p.OnReorderItems(p.selectedItemIDs(), tro) - } - }) menuItems = append(menuItems, fyne.NewMenuItemSeparator(), - remove, - reorder) + remove) } p.menu = widget.NewPopUpMenu( @@ -319,13 +330,8 @@ func (p *PlayQueueList) ensureRadiosMenu() { } }) remove.Icon = theme.ContentRemoveIcon() - reorder := util.NewReorderTracksSubmenu(func(tro sharedutil.TrackReorderOp) { - if p.OnReorderItems != nil { - p.OnReorderItems(p.selectedItemIDs(), tro) - } - }) p.radiosMenu = widget.NewPopUpMenu( - fyne.NewMenu("", remove, reorder), + fyne.NewMenu("", remove), fyne.CurrentApp().Driver().CanvasForObject(p), ) } diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 1537a78c..6cd9c555 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -33,6 +33,9 @@ type TracklistOptions struct { // or to use the number from the track's metadata AutoNumber bool + // Reorderable sets whether the tracklist supports drag-and-drop reordering. + Reorderable bool + // ShowDiscNumber sets whether to display the disc number as part of the '#' column, // (with format %d.%02d). Only applies if AutoNumber==false. ShowDiscNumber bool @@ -74,6 +77,7 @@ type Tracklist struct { OnDownload func(tracks []*mediaprovider.Track, downloadName string) OnShare func(trackID string) OnPlaySongRadio func(track *mediaprovider.Track) + OnReorderTracks func(trackIDs []string, insertPos int) OnShowArtistPage func(artistID string) OnShowAlbumPage func(albumID string) @@ -182,6 +186,17 @@ func NewTracklist(tracks []*mediaprovider.Track, im *backend.ImageManager, useCo t.OnTrackShown(itemID) } }) + t.list.OnDragBegin = func(id int) { + if !t.tracks[id].Selected { + t.selectTrack(id) + t.list.Refresh() + } + } + t.list.OnDragEnd = func(dragged, insertPos int) { + if t.OnReorderTracks != nil { + t.OnReorderTracks(t.SelectedTrackIDs(), insertPos) + } + } t.container = container.NewBorder(t.hdr, nil, nil, nil, t.list) return t } @@ -371,6 +386,7 @@ func (t *Tracklist) CreateRenderer() fyne.WidgetRenderer { } func (t *Tracklist) Refresh() { + t.list.EnableDragging = t.Options.Reorderable t.hdr.DisableSorting = t.Options.DisableSorting t.BaseWidget.Refresh() }