add reorder tracks options to playlist page context menu
This commit is contained in:
@@ -5,6 +5,8 @@
|
|||||||
### Added
|
### Added
|
||||||
- [#39](https://github.com/dweymouth/supersonic/issues/39) Add caching of artist images
|
- [#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)
|
- [#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
|
### 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
|
- [#90](https://github.com/dweymouth/supersonic/issues/90) Wrong covers get loaded for albums if server has different IDs for album and cover art
|
||||||
|
|||||||
+101
-1
@@ -1,6 +1,11 @@
|
|||||||
package sharedutil
|
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 {
|
func StringSliceContains(slice []string, str string) bool {
|
||||||
for _, s := range slice {
|
for _, s := range slice {
|
||||||
@@ -11,6 +16,15 @@ func StringSliceContains(slice []string, str string) bool {
|
|||||||
return false
|
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 {
|
func FindTrackByID(id string, tracks []*subsonic.Child) *subsonic.Child {
|
||||||
for _, tr := range tracks {
|
for _, tr := range tracks {
|
||||||
if id == tr.ID {
|
if id == tr.ID {
|
||||||
@@ -26,3 +40,89 @@ func TrackIDOrEmptyStr(track *subsonic.Child) string {
|
|||||||
}
|
}
|
||||||
return track.ID
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -53,6 +53,10 @@ func NewPlaylistPage(
|
|||||||
a.tracklist.SetVisibleColumns(conf.TracklistColumns)
|
a.tracklist.SetVisibleColumns(conf.TracklistColumns)
|
||||||
a.tracklist.AutoNumber = true
|
a.tracklist.AutoNumber = true
|
||||||
a.tracklist.AuxiliaryMenuItems = []*fyne.MenuItem{
|
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),
|
fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist),
|
||||||
}
|
}
|
||||||
// connect tracklist actions
|
// connect tracklist actions
|
||||||
@@ -114,6 +118,41 @@ func (a *PlaylistPage) load() {
|
|||||||
a.header.Update(playlist)
|
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() {
|
func (a *PlaylistPage) onRemoveSelectedFromPlaylist() {
|
||||||
a.sm.Server.UpdatePlaylistTracks(a.playlistID, nil, a.tracklist.SelectedTrackIndexes())
|
a.sm.Server.UpdatePlaylistTracks(a.playlistID, nil, a.tracklist.SelectedTrackIndexes())
|
||||||
a.tracklist.UnselectAll()
|
a.tracklist.UnselectAll()
|
||||||
|
|||||||
Reference in New Issue
Block a user