From 0c1b293f68d58c8943e04fe9514fc364c5836161 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 25 Mar 2023 11:18:22 -0700 Subject: [PATCH] add reorder tracks options to playlist page context menu --- CHANGELOG.md | 2 + sharedutil/sharedutil.go | 102 +++++++++++++++++++++++++++++++++- sharedutil/sharedutil_test.go | 91 ++++++++++++++++++++++++++++++ ui/browsing/playlistpage.go | 39 +++++++++++++ 4 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 sharedutil/sharedutil_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index b639249..c2f2808 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Added - [#39](https://github.com/dweymouth/supersonic/issues/39) Add caching of artist images - [#94](https://github.com/dweymouth/supersonic/issues/94) Add Cmd+[ Cmd+] back/forward shortcuts for Mac (alongside existing Cmd+Left/Right) +- [#96](https://github.com/dweymouth/supersonic/issues/96) Make scrobbling thresholds configurable +- [#21](https://github.com/dweymouth/supersonic/issues/21) Add ability to reorder tracks within a playlist (via context menu) ### Fixed - [#90](https://github.com/dweymouth/supersonic/issues/90) Wrong covers get loaded for albums if server has different IDs for album and cover art diff --git a/sharedutil/sharedutil.go b/sharedutil/sharedutil.go index 6966a23..3077bdb 100644 --- a/sharedutil/sharedutil.go +++ b/sharedutil/sharedutil.go @@ -1,6 +1,11 @@ package sharedutil -import "github.com/dweymouth/go-subsonic/subsonic" +import ( + "math" + "sort" + + "github.com/dweymouth/go-subsonic/subsonic" +) func StringSliceContains(slice []string, str string) bool { for _, s := range slice { @@ -11,6 +16,15 @@ func StringSliceContains(slice []string, str string) bool { return false } +func IntSliceContains(slice []int, i int) bool { + for _, x := range slice { + if x == i { + return true + } + } + return false +} + func FindTrackByID(id string, tracks []*subsonic.Child) *subsonic.Child { for _, tr := range tracks { if id == tr.ID { @@ -26,3 +40,89 @@ func TrackIDOrEmptyStr(track *subsonic.Child) string { } return track.ID } + +type TrackReorderOp int + +const ( + MoveToTop TrackReorderOp = iota + MoveToBottom + MoveUp + MoveDown +) + +// Reorder tracks and return a new track slice. +// idxToMove must contain only valid indexes into tracks, and no repeats +func ReorderTracks(tracks []*subsonic.Child, idxToMove []int, op TrackReorderOp) []*subsonic.Child { + newTracks := make([]*subsonic.Child, len(tracks)) + switch op { + case MoveToTop: + topIdx := 0 + botIdx := len(idxToMove) + for i, t := range tracks { + if IntSliceContains(idxToMove, i) { + newTracks[topIdx] = t + topIdx++ + } else { + newTracks[botIdx] = t + botIdx++ + } + } + case MoveToBottom: + topIdx := 0 + botIdx := len(tracks) - len(idxToMove) + for i, t := range tracks { + if IntSliceContains(idxToMove, i) { + newTracks[botIdx] = t + botIdx++ + } else { + newTracks[topIdx] = t + topIdx++ + } + } + case MoveUp: + first := firstIdxCanMoveUp(idxToMove) + copy(newTracks, tracks) + for _, i := range idxToMove { + if i < first { + continue + } + newTracks[i-1], newTracks[i] = newTracks[i], newTracks[i-1] + } + case MoveDown: + last := lastIdxCanMoveDown(idxToMove, len(tracks)) + copy(newTracks, tracks) + for i := len(idxToMove) - 1; i >= 0; i-- { + idx := idxToMove[i] + if idx > last { + continue + } + newTracks[idx+1], newTracks[idx] = newTracks[idx], newTracks[idx+1] + } + } + return newTracks +} + +func firstIdxCanMoveUp(idxs []int) int { + prevIdx := -1 + sort.Ints(idxs) + for _, idx := range idxs { + if idx > prevIdx+1 { + return idx + } + prevIdx = idx + } + return math.MaxInt +} + +func lastIdxCanMoveDown(idxs []int, lenSlice int) int { + prevIdx := lenSlice + sort.Ints(idxs) + for i := len(idxs) - 1; i >= 0; i-- { + idx := idxs[i] + if idx < prevIdx-1 { + return idx + } + prevIdx = idx + } + return -1 +} diff --git a/sharedutil/sharedutil_test.go b/sharedutil/sharedutil_test.go new file mode 100644 index 0000000..0aa2fe7 --- /dev/null +++ b/sharedutil/sharedutil_test.go @@ -0,0 +1,91 @@ +package sharedutil + +import ( + "testing" + + "github.com/dweymouth/go-subsonic/subsonic" +) + +func Test_ReorderTracks(t *testing.T) { + tracks := []*subsonic.Child{ + {ID: "a"}, // 0 + {ID: "b"}, // 1 + {ID: "c"}, // 2 + {ID: "d"}, // 3 + {ID: "e"}, // 4 + {ID: "f"}, // 5 + } + + // test MoveToTop: + idxToMove := []int{0, 2, 3, 5} + want := []*subsonic.Child{ + {ID: "a"}, + {ID: "c"}, + {ID: "d"}, + {ID: "f"}, + {ID: "b"}, + {ID: "e"}, + } + newTracks := ReorderTracks(tracks, idxToMove, MoveToTop) + if !tracklistsEqual(t, newTracks, want) { + t.Error("ReorderTracks: MoveToTop order incorrect") + } + + // test MoveToBottom: + idxToMove = []int{0, 2, 5} + want = []*subsonic.Child{ + {ID: "b"}, + {ID: "d"}, + {ID: "e"}, + {ID: "a"}, + {ID: "c"}, + {ID: "f"}, + } + newTracks = ReorderTracks(tracks, idxToMove, MoveToBottom) + if !tracklistsEqual(t, newTracks, want) { + t.Error("ReorderTracks: MoveToBottom order incorrect") + } + + // test MoveUp: + idxToMove = []int{0, 1, 3, 5} + want = []*subsonic.Child{ + {ID: "a"}, + {ID: "b"}, + {ID: "d"}, + {ID: "c"}, + {ID: "f"}, + {ID: "e"}, + } + newTracks = ReorderTracks(tracks, idxToMove, MoveUp) + if !tracklistsEqual(t, newTracks, want) { + t.Error("ReorderTracks: MoveUp order incorrect") + } + + // test MoveDown: + idxToMove = []int{2, 4, 5} + want = []*subsonic.Child{ + {ID: "a"}, + {ID: "b"}, + {ID: "d"}, + {ID: "c"}, + {ID: "e"}, + {ID: "f"}, + } + newTracks = ReorderTracks(tracks, idxToMove, MoveDown) + if !tracklistsEqual(t, newTracks, want) { + t.Error("ReorderTracks: MoveDown order incorrect") + } +} + +func tracklistsEqual(t *testing.T, a, b []*subsonic.Child) bool { + t.Helper() + if len(a) != len(b) { + return false + } + for i, _ := range a { + if a[i].ID != b[i].ID { + return false + } + } + return true +} diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 714a46a..ba9ff3c 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -53,6 +53,10 @@ func NewPlaylistPage( a.tracklist.SetVisibleColumns(conf.TracklistColumns) a.tracklist.AutoNumber = true a.tracklist.AuxiliaryMenuItems = []*fyne.MenuItem{ + fyne.NewMenuItem("Move to top", a.onMoveSelectedToTop), + fyne.NewMenuItem("Move up", a.onMoveSelectedUp), + fyne.NewMenuItem("Move down", a.onMoveSelectedDown), + fyne.NewMenuItem("Move to bottom", a.onMoveSelectedToBottom), fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist), } // connect tracklist actions @@ -114,6 +118,41 @@ func (a *PlaylistPage) load() { a.header.Update(playlist) } +func (a *PlaylistPage) onMoveSelectedToTop() { + a.doSetNewTrackOrder(sharedutil.MoveToTop) +} + +func (a *PlaylistPage) onMoveSelectedUp() { + a.doSetNewTrackOrder(sharedutil.MoveUp) +} + +func (a *PlaylistPage) onMoveSelectedDown() { + a.doSetNewTrackOrder(sharedutil.MoveDown) +} + +func (a *PlaylistPage) onMoveSelectedToBottom() { + a.doSetNewTrackOrder(sharedutil.MoveToBottom) +} + +func (a *PlaylistPage) doSetNewTrackOrder(op sharedutil.TrackReorderOp) { + idxs := a.tracklist.SelectedTrackIndexes() + newTracks := sharedutil.ReorderTracks(a.tracklist.Tracks, idxs, op) + ids := make([]string, len(newTracks)) + for i, tr := range newTracks { + ids[i] = tr.ID + } + err := a.sm.Server.CreatePlaylistWithTracks(ids, map[string]string{ + "playlistId": a.playlistID, + }) + if err != nil { + log.Printf("error updating playlist: %s", err.Error()) + } else { + a.tracklist.Tracks = newTracks + a.tracklist.UnselectAll() + a.tracklist.Refresh() + } +} + func (a *PlaylistPage) onRemoveSelectedFromPlaylist() { a.sm.Server.UpdatePlaylistTracks(a.playlistID, nil, a.tracklist.SelectedTrackIndexes()) a.tracklist.UnselectAll()