From 331fa1d952d7e8e7c587e5cf63987f2213171a17 Mon Sep 17 00:00:00 2001 From: Deluan Date: Thu, 4 Jun 2020 19:05:41 -0400 Subject: [PATCH] Add ability to reorder playlist items --- model/playlist.go | 1 + persistence/playlist_track_repository.go | 36 ++++++++--- server/app/app.go | 5 +- server/app/playlists.go | 46 ++++++++++++-- ui/package.json | 1 + ui/src/playlist/PlaylistSongs.js | 78 ++++++++++++++++++------ utils/strings.go | 13 ++++ utils/strings_test.go | 12 ++++ 8 files changed, 158 insertions(+), 34 deletions(-) diff --git a/model/playlist.go b/model/playlist.go index ff0f1d7b3..cd20da632 100644 --- a/model/playlist.go +++ b/model/playlist.go @@ -43,4 +43,5 @@ type PlaylistTrackRepository interface { Add(mediaFileIds []string) error Update(mediaFileIds []string) error Delete(id string) error + Reorder(pos int, newPos int) error } diff --git a/persistence/playlist_track_repository.go b/persistence/playlist_track_repository.go index 92f652f52..6f53d0054 100644 --- a/persistence/playlist_track_repository.go +++ b/persistence/playlist_track_repository.go @@ -4,6 +4,7 @@ import ( . "github.com/Masterminds/squirrel" "github.com/deluan/navidrome/log" "github.com/deluan/navidrome/model" + "github.com/deluan/navidrome/utils" "github.com/deluan/rest" ) @@ -70,18 +71,10 @@ func (r *playlistTrackRepository) Add(mediaFileIds []string) error { log.Debug(r.ctx, "Adding songs to playlist", "playlistId", r.playlistId, "mediaFileIds", mediaFileIds) } - // Get all current tracks - all := r.newSelect().Columns("media_file_id").Where(Eq{"playlist_id": r.playlistId}).OrderBy("id") - var tracks model.PlaylistTracks - err := r.queryAll(all, &tracks) + ids, err := r.getTracks() if err != nil { - log.Error("Error querying current tracks from playlist", "playlistId", r.playlistId, err) return err } - ids := make([]string, len(tracks)) - for i := range tracks { - ids[i] = tracks[i].MediaFileID - } // Append new tracks ids = append(ids, mediaFileIds...) @@ -90,6 +83,22 @@ func (r *playlistTrackRepository) Add(mediaFileIds []string) error { return r.Update(ids) } +func (r *playlistTrackRepository) getTracks() ([]string, error) { + // Get all current tracks + all := r.newSelect().Columns("media_file_id").Where(Eq{"playlist_id": r.playlistId}).OrderBy("id") + var tracks model.PlaylistTracks + err := r.queryAll(all, &tracks) + if err != nil { + log.Error("Error querying current tracks from playlist", "playlistId", r.playlistId, err) + return nil, err + } + ids := make([]string, len(tracks)) + for i := range tracks { + ids[i] = tracks[i].MediaFileID + } + return ids, nil +} + func (r *playlistTrackRepository) Update(mediaFileIds []string) error { // Remove old tracks del := Delete(r.tableName).Where(Eq{"playlist_id": r.playlistId}) @@ -156,4 +165,13 @@ func (r *playlistTrackRepository) Delete(id string) error { return r.updateStats() } +func (r *playlistTrackRepository) Reorder(pos int, newPos int) error { + ids, err := r.getTracks() + if err != nil { + return err + } + newOrder := utils.MoveString(ids, pos-1, newPos-1) + return r.Update(newOrder) +} + var _ model.PlaylistTrackRepository = (*playlistTrackRepository)(nil) diff --git a/server/app/app.go b/server/app/app.go index 0ad730a47..5f317ce23 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -91,7 +91,7 @@ func (app *Router) RX(r chi.Router, pathPrefix string, constructor rest.Reposito type restHandler = func(rest.RepositoryConstructor, ...rest.Logger) http.HandlerFunc func (app *Router) addPlaylistTrackRoute(r chi.Router) { - // Add a middleware to capture the playlisId + // Add a middleware to capture the playlistId wrapper := func(f restHandler) http.HandlerFunc { return func(res http.ResponseWriter, req *http.Request) { c := func(ctx context.Context) rest.Repository { @@ -109,6 +109,9 @@ func (app *Router) addPlaylistTrackRoute(r chi.Router) { r.Route("/{id}", func(r chi.Router) { r.Use(UrlParams) r.Get("/", wrapper(rest.Get)) + r.Put("/", func(w http.ResponseWriter, r *http.Request) { + reorderItem(app.ds)(w, r) + }) r.Delete("/", func(w http.ResponseWriter, r *http.Request) { deleteFromPlaylist(app.ds)(w, r) }) diff --git a/server/app/playlists.go b/server/app/playlists.go index 7df1a77e5..e1348efe3 100644 --- a/server/app/playlists.go +++ b/server/app/playlists.go @@ -4,16 +4,13 @@ import ( "encoding/json" "fmt" "net/http" + "strconv" "github.com/deluan/navidrome/log" "github.com/deluan/navidrome/model" "github.com/deluan/navidrome/utils" ) -type addTracksPayload struct { - Ids []string `json:"ids"` -} - func deleteFromPlaylist(ds model.DataStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { playlistId := utils.ParamString(r, ":playlistId") @@ -38,6 +35,10 @@ func deleteFromPlaylist(ds model.DataStore) http.HandlerFunc { } func addToPlaylist(ds model.DataStore) http.HandlerFunc { + type addTracksPayload struct { + Ids []string `json:"ids"` + } + return func(w http.ResponseWriter, r *http.Request) { playlistId := utils.ParamString(r, ":playlistId") tracksRepo := ds.Playlist(r.Context()).Tracks(playlistId) @@ -60,3 +61,40 @@ func addToPlaylist(ds model.DataStore) http.HandlerFunc { } } } + +func reorderItem(ds model.DataStore) http.HandlerFunc { + type reorderPayload struct { + InsertBefore string `json:"insert_before"` + } + + return func(w http.ResponseWriter, r *http.Request) { + playlistId := utils.ParamString(r, ":playlistId") + id := utils.ParamInt(r, ":id", 0) + if id == 0 { + http.Error(w, "invalid id", http.StatusBadRequest) + return + } + tracksRepo := ds.Playlist(r.Context()).Tracks(playlistId) + var payload reorderPayload + err := json.NewDecoder(r.Body).Decode(&payload) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + newPos, err := strconv.Atoi(payload.InsertBefore) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + err = tracksRepo.Reorder(id, newPos) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + _, err = w.Write([]byte(fmt.Sprintf(`{"id":"%d"}`, id))) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } +} diff --git a/ui/package.json b/ui/package.json index 9c3b5c2ec..64d7bfe7f 100644 --- a/ui/package.json +++ b/ui/package.json @@ -13,6 +13,7 @@ "react": "^16.13.1", "react-admin": "^3.5.3", "react-dom": "^16.13.1", + "react-drag-listview": "^0.1.6", "react-jinke-music-player": "^4.13.1", "react-measure": "^2.3.0", "react-redux": "^7.2.0", diff --git a/ui/src/playlist/PlaylistSongs.js b/ui/src/playlist/PlaylistSongs.js index 5243aa82d..d8d8a76f9 100644 --- a/ui/src/playlist/PlaylistSongs.js +++ b/ui/src/playlist/PlaylistSongs.js @@ -6,10 +6,13 @@ import { TextField, useListController, useRefresh, + useDataProvider, + useNotify, } from 'react-admin' import classnames from 'classnames' import { Card, useMediaQuery } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' +import ReactDragListView from 'react-drag-listview' import { DurationField, SongDetails, @@ -45,6 +48,9 @@ const useStyles = makeStyles( flexWrap: 'wrap', }, noResults: { padding: 20 }, + draggable: { + cursor: 'move', + }, }), { name: 'RaList' } ) @@ -61,16 +67,29 @@ const PlaylistSongs = (props) => { const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs')) const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md')) const controllerProps = useListController(props) + const dataProvider = useDataProvider() const refresh = useRefresh() + const notify = useNotify() const { bulkActionButtons, expand, className, playlistId } = props - const { data, ids, version, loaded } = controllerProps + const { data, ids, version } = controllerProps const anySong = data[ids[0]] const showPlaceholder = !anySong || anySong.playlistId !== playlistId const hasBulkActions = props.bulkActionButtons !== false - if (loaded && ids.length === 0) { - return
+ const reorder = (playlistId, id, newPos) => { + dataProvider + .update('playlistTrack', { + id, + data: { insert_before: newPos }, + filter: { playlist_id: playlistId }, + }) + .then(() => { + refresh() + }) + .catch(() => { + notify('ra.page.error', 'warning') + }) } const onAddToPlaylist = (pls) => { @@ -79,6 +98,12 @@ const PlaylistSongs = (props) => { } } + const handleDragEnd = (from, to) => { + const toId = ids[to] + const fromId = ids[from] + reorder(playlistId, fromId, toId) + } + return ( <> { size={'small'} /> ) : ( - } - rowClick={null} - {...controllerProps} - hasBulkActions={hasBulkActions} - contextAlwaysVisible={!isDesktop} - > - {isDesktop && } - - {isDesktop && } - {isDesktop && } - - - + + } + rowClick={null} + {...controllerProps} + hasBulkActions={hasBulkActions} + contextAlwaysVisible={!isDesktop} + > + {isDesktop && ( + + )} + + {isDesktop && } + {isDesktop && ( + + )} + + + + )}
diff --git a/utils/strings.go b/utils/strings.go index bc05a88ea..9b9806d7a 100644 --- a/utils/strings.go +++ b/utils/strings.go @@ -25,3 +25,16 @@ func StringInSlice(a string, list []string) bool { } return false } + +func InsertString(array []string, value string, index int) []string { + return append(array[:index], append([]string{value}, array[index:]...)...) +} + +func RemoveString(array []string, index int) []string { + return append(array[:index], array[index+1:]...) +} + +func MoveString(array []string, srcIndex int, dstIndex int) []string { + value := array[srcIndex] + return InsertString(RemoveString(array, srcIndex), value, dstIndex) +} diff --git a/utils/strings_test.go b/utils/strings_test.go index fda705d4d..5375b3301 100644 --- a/utils/strings_test.go +++ b/utils/strings_test.go @@ -48,4 +48,16 @@ var _ = Describe("Strings", func() { Expect(StringInSlice("bbb", []string{"bbb", "aaa", "ccc"})).To(BeTrue()) }) }) + + Describe("MoveString", func() { + It("moves item to end of slice", func() { + Expect(MoveString([]string{"1", "2", "3"}, 0, 2)).To(ConsistOf("2", "3", "1")) + }) + It("moves item to beginning of slice", func() { + Expect(MoveString([]string{"1", "2", "3"}, 2, 0)).To(ConsistOf("3", "1", "2")) + }) + It("keeps item in same position if srcIndex == dstIndex", func() { + Expect(MoveString([]string{"1", "2", "3"}, 1, 1)).To(ConsistOf("1", "2", "3")) + }) + }) })