diff --git a/src/js/services/core/actions.js b/src/js/services/core/actions.js index e97e271d..914359a6 100755 --- a/src/js/services/core/actions.js +++ b/src/js/services/core/actions.js @@ -371,9 +371,7 @@ export function reorderPlaylistTracks(uri, indexes, insert_before, snapshot_id = insert_before, snapshot_id, }; - - case 'm3u': - case 'gmusic': + default: return { type: 'MOPIDY_REORDER_PLAYLIST_TRACKS', key: uri, @@ -381,40 +379,22 @@ export function reorderPlaylistTracks(uri, indexes, insert_before, snapshot_id = range_length: range.length, insert_before, }; - - default: - return { - type: 'UNSUPPORTED_ACTION', - name: 'reorderPlaylistTracks', - }; } } -export function savePlaylist(uri, name, description = '', is_public = false, is_collaborative = false, image = null) { +export function savePlaylist(uri, data) { switch (uriSource(uri)) { case 'spotify': return { type: 'SPOTIFY_SAVE_PLAYLIST', key: uri, - name, - description: (description == '' ? null : description), - image, - is_public, - is_collaborative, + data, }; - - case 'm3u': - case 'gmusic': + default: return { type: 'MOPIDY_SAVE_PLAYLIST', key: uri, - name, - }; - - default: - return { - type: 'UNSUPPORTED_ACTION', - name: 'savePlaylist', + data, }; } } @@ -432,7 +412,6 @@ export function deletePlaylist(uri) { switch (uriSource(uri)) { case 'spotify': return spotifyActions.following(uri, 'DELETE'); - default: return mopidyActions.deletePlaylist(uri); } @@ -446,20 +425,12 @@ export function removeTracksFromPlaylist(uri, tracks_indexes) { key: uri, tracks_indexes, }; - - case 'm3u': - case 'gmusic': + default: return { type: 'MOPIDY_REMOVE_PLAYLIST_TRACKS', key: uri, tracks_indexes, }; - - default: - return { - type: 'UNSUPPORTED_ACTION', - name: 'removeTracksFromPlaylist', - }; } } @@ -471,20 +442,12 @@ export function addTracksToPlaylist(uri, tracks_uris) { key: uri, tracks_uris, }; - - case 'm3u': - case 'gmusic': + default: return { type: 'MOPIDY_ADD_PLAYLIST_TRACKS', key: uri, tracks_uris, }; - - default: - return { - type: 'UNSUPPORTED_ACTION', - name: 'addTracksToPlaylist', - }; } } diff --git a/src/js/services/core/middleware.js b/src/js/services/core/middleware.js index be1a4b6b..2fe79c2f 100755 --- a/src/js/services/core/middleware.js +++ b/src/js/services/core/middleware.js @@ -275,10 +275,8 @@ const CoreMiddleware = (function () { case 'spotify': store.dispatch(spotifyActions.getPlaylist(key, {})); break; - case 'm3u': - store.dispatch(mopidyActions.getPlaylist(key, {})); - break; default: + store.dispatch(mopidyActions.getPlaylist(key, {})); break; } next(action); diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index 227f094c..621ed765 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -1423,7 +1423,7 @@ const MopidyMiddleware = (function () { // requires a Mopidy playlist object (with updates) request(store, 'playlists.lookup', { uri: action.key }) .then((response) => { - const mopidy_playlist = { ...response, name: action.name }; + const mopidy_playlist = { ...response, ...action.data }; request(store, 'playlists.save', { playlist: mopidy_playlist }) .then((response) => { @@ -1432,9 +1432,9 @@ const MopidyMiddleware = (function () { // Overwrite our playlist with the response to our save // This is essential to get the updated URI from Mopidy const playlist = { + ...action.data, ...store.getState().core.items[action.key], - uri: response.uri, - name: response.name, + ...response, }; // When we rename a playlist, the URI also changes to reflect the name change. @@ -2154,134 +2154,57 @@ const MopidyMiddleware = (function () { case 'MOPIDY_GET_LIBRARY_PLAYLISTS': { store.dispatch(uiActions.startProcess(action.type, { notification: false })); - // Built-in playlist support works differently to other providers - if (action.uri === 'm3u:playlists') { - request(store, 'playlists.asList') - .then((listResponse) => { - const libraryPlaylists = []; - const playlist_uris = arrayOf('uri', listResponse).filter( - (pUri) => (pUri.indexOf('m3u') > -1), - ); - store.dispatch( - uiActions.updateProcess( - action.type, - { - total: playlist_uris.length, - remaining: playlist_uris.length, - }, - ), - ); + request(store, 'playlists.asList').then((browseResponse) => { + const allUris = arrayOf('uri', browseResponse); + store.dispatch( + uiActions.updateProcess( + action.type, + { + total: allUris.length, + remaining: allUris.length, + }, + ), + ); - if (playlist_uris.length) { - playlist_uris.forEach((uri, index) => { - request(store, 'playlists.lookup', { uri }) - .then((response) => { - if (response) { - libraryPlaylists.push( - formatPlaylist({ - name: response.name, - uri: response.uri, - can_edit: uriSource(response.uri) === 'm3u', - last_modified: response.last_modified, - // By not including actual tracks they will be fetched when needed. We don't - // want these simple tracks because they don't contain duration, artist, etc. - tracks_total: response.tracks ? response.tracks.length : null, - }), - ); - } + const run = () => { + if (allUris.length) { + const uri = allUris.splice(0, 1)[0]; + const processor = store.getState().ui.processes[action.type]; - store.dispatch( - uiActions.updateProcess( - action.type, - { - remaining: playlist_uris.length - index - 1, - }, - ), - ); - - if (index === playlist_uris.length - 1) { - store.dispatch(coreActions.itemsLoaded(libraryPlaylists)); - store.dispatch(coreActions.libraryLoaded({ - uri: action.uri, - type: 'playlists', - items_uris: arrayOf('uri', libraryPlaylists), - })); - store.dispatch(uiActions.processFinished(action.type)); - } - }); - }); - } else { - store.dispatch(coreActions.libraryLoaded({ - uri: action.uri, - type: 'playlists', - items_uris: [], - })); - store.dispatch(uiActions.stopLoading('mopidy:library:playlists')); - store.dispatch(uiActions.processFinished(action.type)); + if (processor && processor.status === 'cancelling') { + store.dispatch(uiActions.processCancelled(action.type)); + store.dispatch(uiActions.stopLoading(action.uri)); + return; } - }); - } else { - request(store, 'library.browse', { uri: action.uri }) - .then((browseResponse) => { - const libraryPlaylists = []; + store.dispatch(uiActions.updateProcess(action.type, { remaining: allUris.length })); - store.dispatch( - uiActions.updateProcess( - action.type, - { - total: browseResponse.length, - remaining: browseResponse.length, - }, - ), - ); + request(store, 'playlists.lookup', { uri }).then((lookupResponse) => { + if (lookupResponse) { + const playlist = formatPlaylist({ + name: lookupResponse.name, + uri: lookupResponse.uri, + can_edit: true, // TODO: Confirm whether some are uneditable?? + last_modified: lookupResponse.last_modified, + // Don't include simple tracks; they don't contain duration, artist, etc. + tracks_total: lookupResponse?.tracks?.length || null, + }); - if (browseResponse.length) { - browseResponse.forEach((playlist, index) => { - request(store, 'library.lookup', { uris: [playlist.uri] }) - .then((response) => { - if (response) { - libraryPlaylists.push( - formatPlaylist({ - name: playlist.name, - uri: playlist.uri, - can_edit: uriSource(playlist.uri) === 'm3u', - last_modified: playlist.last_modified, - tracks: formatTracks(response[playlist.uri]), - }), - ); - } + store.dispatch(coreActions.itemLoaded(playlist)); + } + run(); + }); + } else { + store.dispatch(uiActions.processFinished(action.type)); + store.dispatch(coreActions.libraryLoaded({ + uri: action.uri, + type: 'playlists', + items_uris: arrayOf('uri', browseResponse), + })); + } + }; - store.dispatch( - uiActions.updateProcess( - action.type, - { - remaining: browseResponse.length - index - 1, - }, - ), - ); - - if (index === browseResponse.length - 1) { - store.dispatch(coreActions.itemsLoaded(libraryPlaylists)); - store.dispatch(coreActions.libraryLoaded({ - uri: action.uri, - type: 'playlists', - items_uris: arrayOf('uri', libraryPlaylists), - })); - store.dispatch(uiActions.processFinished(action.type)); - } - }); - }); - } else { - store.dispatch(coreActions.libraryLoaded({ - uri: action.uri, - type: 'playlists', - items_uris: [], - })); - store.dispatch(uiActions.stopLoading('mopidy:library:playlists')); - store.dispatch(uiActions.processFinished(action.type)); - } - }); - } + run(); + }); break; } diff --git a/src/js/services/spotify/actions.js b/src/js/services/spotify/actions.js index 474601fe..be9a269d 100755 --- a/src/js/services/spotify/actions.js +++ b/src/js/services/spotify/actions.js @@ -1237,14 +1237,8 @@ export function createPlaylist(playlist) { }; } -export function savePlaylist(uri, name, description, is_public, is_collaborative, image) { +export function savePlaylist(uri, { image, ...data }) { return (dispatch, getState) => { - const data = { - name, - description, - public: is_public, - collaborative: is_collaborative, - }; const { spotify: { me: { diff --git a/src/js/util/selectors.js b/src/js/util/selectors.js index 933cd9d2..03782f69 100755 --- a/src/js/util/selectors.js +++ b/src/js/util/selectors.js @@ -121,6 +121,11 @@ const providers = { uri: 'jellyfin:playlists', title: i18n('services.jellyfin.title'), }, + { + scheme: 'ytmusic:', + uri: 'ytmusic:playlists', + title: i18n('services.youtube.title'), + }, ], albums: [ { diff --git a/src/js/views/Modals/CreatePlaylist.js b/src/js/views/Modals/CreatePlaylist.js index 114f2365..109cc27b 100755 --- a/src/js/views/Modals/CreatePlaylist.js +++ b/src/js/views/Modals/CreatePlaylist.js @@ -154,6 +154,18 @@ const CreatePlaylist = () => { + + onChange({ scheme: e.target.value })} + /> + + + + { - const { - uri, - playlist, - mopidyActions: { - getPlaylist, - }, - } = this.props; - - if (playlist !== prevPlaylist) { - getPlaylist(uri); - } - } - - static getDerivedStateFromProps({ playlist }, state) { - if (playlist && !state.loaded) { - return { - loaded: true, - name: playlist.name, - description: playlist.description, - public: (playlist.public === true), - collaborative: (playlist.collaborative === true), - }; - } - return null; - } - - setImage = (e) => { - const self = this; - - // Create a file-reader to import the selected image as a base64 string - const file_reader = new FileReader(); - - // Once the image is loaded, convert the result - file_reader.addEventListener('load', (e) => { - const image_base64 = e.target.result.replace('data:image/jpeg;base64,', ''); - self.setState({ image: image_base64 }); - }); - - // This calls the filereader to load the file - file_reader.readAsDataURL(e.target.files[0]); - } - - savePlaylist = (e) => { - const { - name, - description, - public: isPublic, - collaborative, - image, - } = this.state; - const { - uri, - coreActions: { - savePlaylist, - }, - uiActions: { - closeModal, - }, - } = this.props; - - e.preventDefault(); - - if (!name || name == '') { - this.setState({ error: i18n('modal.edit_playlist.name_required') }); - return false; - } - savePlaylist( - uri, - name, - description, - isPublic, - collaborative, - image, - ); - closeModal(); - return false; - } - - renderFields = () => { - const { - uri, - } = this.props; - const { - name, - description, - public: isPublic, - collaborative, - image, - } = this.state; - - switch (uriSource(uri)) { - case 'spotify': - return ( - - - - - - - this.setState({ name: e.target.value })} - value={name} - /> - - - - - - - - this.setState({ description: e.target.value })} - value={description} - /> - - - - - - - - this.setImage(e)} - /> - - - - - - - - - - - - this.setState({ public: !isPublic })} - /> - - - - - - this.setState({ collaborative: !collaborative })} - /> - - - - - - - - ); - - default: - return ( - - - - - - - this.setState({ name: e.target.value })} - value={name} - /> - - - - ); - } - } - - render = () => { - const { - error, - } = this.state; - - return ( - - - - - {error ? {error} : null} - this.savePlaylist(e)}> - - {this.renderFields()} - - - - - - - - - ); - } -} - -const mapStateToProps = (state, ownProps) => { - const uri = decodeUri(ownProps.match.params.uri); - const itemSelector = makeItemSelector(uri); - const loadingSelector = makeLoadingSelector([`(.*)${uri}(.*)`, '^((?!contains).)*$', '^((?!tracks).)*$']); - - return { - uri, - playlist: itemSelector(state), - loading: loadingSelector(state), - }; +const EMPTY_FORM = { + loaded: false, + error: null, + name: '', + description: '', + image: null, + public: false, + collaborative: false, }; -const mapDispatchToProps = (dispatch) => ({ - coreActions: bindActionCreators(coreActions, dispatch), - uiActions: bindActionCreators(uiActions, dispatch), - mopidyActions: bindActionCreators(mopidyActions, dispatch), - spotifyActions: bindActionCreators(spotifyActions, dispatch), -}); +const Fields = ({ + uri, + form, + onChange, + onChangeImage, +}) => ( + + + + + + + + + + + {uriSource(uri) === 'spotify' && ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + + +); -export default connect(mapStateToProps, mapDispatchToProps)(EditPlaylist); +const EditPlaylist = () => { + const { uri: encodedUri } = useParams(); + const uri = decodeUri(encodedUri); + const itemSelector = makeItemSelector(uri); + const playlist = useSelector(itemSelector); + const dispatch = useDispatch(); + const [form, setForm] = useState(EMPTY_FORM); + const [error, setError] = useState(); + + useEffect(() => { + dispatch(setWindowTitle(i18n('modal.edit_playlist.title'))); + + if (uri && !playlist) dispatch(loadPlaylist(uri)); + }, []); + + useEffect(() => { + setForm({ + loaded: true, + name: playlist?.name || '', + description: playlist?.description || '', + public: (playlist?.public === true), + collaborative: (playlist?.collaborative === true), + }); + }, [playlist]); + + const onChange = ({ target: { name, type, value, checked }}) => + setForm((prev) => ({ + ...prev, + [name]: type === 'checkbox' ? checked : value, + })); + + const onChangeImage = (e) => { + const file_reader = new FileReader(); + file_reader.addEventListener('load', (e) => { + const image_base64 = e.target.result.replace('form:image/jpeg;base64,', ''); + setForm({ image: image_base64 }); + }); + file_reader.readAsDataURL(e.target.files[0]); + } + + const onSubmit = (e) => { + e.preventDefault(); + + if (form.name == '') { + setError(i18n('modal.edit_playlist.name_required')); + return; + } + const data = uriSource(uri) === 'spotify' + ? form + : { name: form.name }; + dispatch(savePlaylist(uri, data)); + dispatch(closeModal()); + } + + return ( + + + + + {error && {error}} + + + + + + + + + + + ); +} + +export default EditPlaylist; diff --git a/src/js/views/Playlist.js b/src/js/views/Playlist.js index d9716981..2b225fd0 100755 --- a/src/js/views/Playlist.js +++ b/src/js/views/Playlist.js @@ -40,27 +40,6 @@ const Actions = ({ handleContextMenu, }) => { switch (uriSource(uri)) { - case 'm3u': - return ( - - - - - - - - - - - ); - case 'spotify': if (can_edit) { return ( @@ -111,6 +90,12 @@ const Actions = ({ > + + +