From 9d56a05fc4a41114dee53e43938f74985524ac6e Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Tue, 4 Apr 2017 21:21:44 +1200 Subject: [PATCH 1/9] Batch processor; Merging enqueue and enqueue next --- src/js/components/ContextMenu.js | 33 +++++------- src/js/services/mopidy/actions.js | 12 +---- src/js/services/mopidy/middleware.js | 81 ++++++++++++++-------------- 3 files changed, 57 insertions(+), 69 deletions(-) diff --git a/src/js/components/ContextMenu.js b/src/js/components/ContextMenu.js index cacadd05..56e55114 100755 --- a/src/js/components/ContextMenu.js +++ b/src/js/components/ContextMenu.js @@ -139,7 +139,7 @@ class ContextMenu extends React.Component{ return (helpers.uriSource(this.props.menu.items[0].uri) == 'spotify') } - toggleInLibrary(in_library){ + toggleInLibrary(e, in_library){ this.props.uiActions.hideContextMenu() if (in_library){ this.props.spotifyActions.following(this.props.menu.items[0].uri, 'DELETE') @@ -148,13 +148,13 @@ class ContextMenu extends React.Component{ } } - playQueueItem(){ + playQueueItem(e){ this.props.uiActions.hideContextMenu() var tracks = this.props.menu.items; this.props.mopidyActions.changeTrack( tracks[0].tlid ) } - removeFromQueue(){ + removeFromQueue(e){ this.props.uiActions.hideContextMenu() var tracks = this.props.menu.items; var tracks_tlids = []; @@ -164,37 +164,32 @@ class ContextMenu extends React.Component{ this.props.mopidyActions.removeTracks( tracks_tlids ); } - playURIs(){ + playURIs(e){ this.props.uiActions.hideContextMenu() this.props.mopidyActions.playURIs(this.props.menu.uris, this.props.menu.tracklist_uri) } - playURIsNext(){ + addToQueue(e, next = false){ this.props.uiActions.hideContextMenu() - this.props.mopidyActions.enqueueURIsNext(this.props.menu.uris, this.props.menu.tracklist_uri) + this.props.mopidyActions.enqueueURIs(this.props.menu.uris, this.props.menu.tracklist_uri, next) } - addToQueue(){ - this.props.uiActions.hideContextMenu() - this.props.mopidyActions.enqueueURIs(this.props.menu.uris, this.props.menu.tracklist_uri) - } - - addTracksToPlaylist(playlist_uri){ + addTracksToPlaylist(e, playlist_uri){ this.props.uiActions.hideContextMenu() this.props.uiActions.addTracksToPlaylist(playlist_uri, this.props.menu.uris) } - removeFromPlaylist(){ + removeFromPlaylist(e){ this.props.uiActions.hideContextMenu() this.props.uiActions.removeTracksFromPlaylist(this.props.menu.tracklist_uri, this.props.menu.indexes) } - startRadio(){ + startRadio(e){ this.props.uiActions.hideContextMenu() this.props.pusherActions.startRadio(this.props.menu.uris) } - goToArtist(){ + goToArtist(e){ if (!this.props.menu.items || this.props.menu.items.length <= 0 || !this.props.menu.items[0].artists || this.props.menu.items[0].artists.length <= 0){ return null } else { @@ -203,7 +198,7 @@ class ContextMenu extends React.Component{ } } - goToUser(){ + goToUser(e){ if (!this.props.menu.items || this.props.menu.items.length <= 0){ return null } else { @@ -223,7 +218,7 @@ class ContextMenu extends React.Component{ this.props.uiActions.hideContextMenu() } - closeAndDeselectTracks(){ + closeAndDeselectTracks(e){ this.props.uiActions.hideContextMenu(); // TODO } @@ -341,7 +336,7 @@ class ContextMenu extends React.Component{ var play_uris_next = ( - this.playURIsNext(e)}> + this.addToQueue(e, true)}> Play next @@ -367,7 +362,7 @@ class ContextMenu extends React.Component{ var toggle_in_library = ( - this.toggleInLibrary(context.in_library)}> + this.toggleInLibrary(e, context.in_library)}> {context.in_library ? 'Remove from library' : 'Add to library'} diff --git a/src/js/services/mopidy/actions.js b/src/js/services/mopidy/actions.js index 00c2f2bf..847da96f 100755 --- a/src/js/services/mopidy/actions.js +++ b/src/js/services/mopidy/actions.js @@ -57,20 +57,12 @@ export function playURIs( uris, from_uri = null ){ } } -export function enqueueURIsNext( uris, from_uri = null ){ - if( typeof(uris) !== 'object' ) uris = [uris] - return { - type: 'MOPIDY_ENQUEUE_URIS_NEXT', - uris: uris, - from_uri: from_uri - } -} - -export function enqueueURIs( uris, from_uri = null, at_position = false ){ +export function enqueueURIs( uris, from_uri = null, next = false, at_position = null ){ return { type: 'MOPIDY_ENQUEUE_URIS', uris: uris, at_position: at_position, + next: next, from_uri: from_uri } } diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index e75a3c06..c8bf7490 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -222,35 +222,8 @@ const MopidyMiddleware = (function(){ break; case 'MOPIDY_ENQUEUE_URIS': - - // create batches of 20 uris to load - var all_uris = Object.assign([], action.uris) - var batched_uris = [] - while (all_uris.length > 0){ - batched_uris.push( all_uris.splice(0,20) ) - } - - // run each batch - for (var i = 0; i < batched_uris.length; i++){ - var value = { uris: batched_uris[i] } - if (action.at_position){ - value.at_position = action.at_position - } - - instruct( socket, store, 'tracklist.add', value ) - .then( response => { - var tlids = [] - for (var i = 0; i < response.length; i++){ - tlids.push(response[i].tlid) - } - store.dispatch( pusherActions.addQueueMetadata(tlids, action.from_uri) ) - console.info('Added '+tlids.length+' URI(s) to queue') - }) - } - break - - case 'MOPIDY_ENQUEUE_URIS_NEXT': - + var uris_added = 0 + var remaining_uris = Object.assign([], action.uris) var current_track = store.getState().ui.current_track var current_tracklist = store.getState().ui.current_tracklist var current_track_index = -1 @@ -264,18 +237,46 @@ const MopidyMiddleware = (function(){ } } - var at_position = null - if( current_track_index > -1 ) at_position = current_track_index + 1 + let process_batch = function(){ + var params = {uris: remaining_uris.splice(0,10)} + if (action.next && current_track_index > -1){ + params.at_position = current_track_index + uris_added + 1 + } else if (action.at_position){ + params.at_position = action.at_position + } + + instruct(socket, store, 'tracklist.add', params) + .then( response => { + + // append our counter + uris_added += response.length + + // add metadata to queue + var tlids = [] + for (var i = 0; i < response.length; i++){ + tlids.push(response[i].tlid) + } + store.dispatch( pusherActions.addQueueMetadata(tlids, action.from_uri) ) + + console.info('Added '+tlids.length+' URI(s) to queue') + + // still more URIs? run again in 0.8s + // this gives our server time to handle other requests + // crude, but prevents locking the server + if (remaining_uris.length > 0){ + setTimeout( + function(){ + process_batch() + }, + 800 + ) + } + }) + } + + // start processing + process_batch() - instruct( socket, store, 'tracklist.add', { uris: action.uris, at_position: at_position } ) - .then( response => { - var tlids = [] - for (var i = 0; i < response.length; i++){ - tlids.push(response[i].tlid) - } - store.dispatch( pusherActions.addQueueMetadata(tlids, action.from_uri) ) - store.dispatch( uiActions.createNotification('Added '+tlids.length+' URI(s) to queue') ) - }) break case 'MOPIDY_PLAY_URIS': From 6bae5c348c5cc80ec38cfd77c8b4917904af9261 Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Wed, 5 Apr 2017 09:01:20 +1200 Subject: [PATCH 2/9] Multiple notifications are back; Customisable notification.id for rolling updates --- src/js/components/Notifications.js | 49 +++++++++++++++++++--------- src/js/helpers.js | 2 +- src/js/services/mopidy/middleware.js | 18 ++++++---- src/js/services/ui/actions.js | 7 ++-- src/js/services/ui/middleware.js | 16 +++++---- src/js/services/ui/reducer.js | 1 + src/js/views/App.js | 4 +-- src/js/views/Debug.js | 3 +- 8 files changed, 63 insertions(+), 37 deletions(-) diff --git a/src/js/components/Notifications.js b/src/js/components/Notifications.js index 37ccc332..c5651660 100755 --- a/src/js/components/Notifications.js +++ b/src/js/components/Notifications.js @@ -14,22 +14,42 @@ export default class Notifications extends React.Component{ // we only care about the last notification var notification = this.props.notifications[this.props.notifications.length-1] - if (notification.is_shortcut){ - return ( -
- -
- ) - } else { - return ( -
- this.props.uiActions.removeNotification(notification.id) } /> - { notification.content } -
- ) - } + return ( + + { + this.props.notifications.map(notification => { + switch (notification.type){ + case 'shortcut': + return ( +
+ +
+ ) + + case 'loading': + return ( +
+ this.props.uiActions.removeNotification(notification.id) } /> + { notification.content } +
+ ) + + default: + return ( +
+ this.props.uiActions.removeNotification(notification.id) } /> + { notification.content } +
+ ) + } + }) + } +
+ ) } + // do we want the loading of everything to be displayed? + // not likely... renderLoader(){ if (!this.props.load_queue){ return null @@ -59,7 +79,6 @@ export default class Notifications extends React.Component{ return (
{this.renderNotifications()} - {this.renderLoader()}
) } diff --git a/src/js/helpers.js b/src/js/helpers.js index e282c8c9..a1573e08 100755 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -266,7 +266,7 @@ export let asURIs = function(items){ * Merge duplicated items in an array * * @param list Array the unclean array - * @param key string = the unique key (uri, tlid, etc) + * @param key string = the unique key (id, uri, tlid, etc) **/ export let mergeDuplicates = function(list, key){ var clean_list = []; diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index c8bf7490..8d7ea6ec 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -237,8 +237,10 @@ const MopidyMiddleware = (function(){ } } - let process_batch = function(){ - var params = {uris: remaining_uris.splice(0,10)} + let process_batch = function(){ + store.dispatch(uiActions.createNotification('Adding '+remaining_uris.length+' URI(s)', 'loading', action.type)) + + var params = {uris: remaining_uris.splice(0,5)} if (action.next && current_track_index > -1){ params.at_position = current_track_index + uris_added + 1 } else if (action.at_position){ @@ -256,11 +258,9 @@ const MopidyMiddleware = (function(){ for (var i = 0; i < response.length; i++){ tlids.push(response[i].tlid) } - store.dispatch( pusherActions.addQueueMetadata(tlids, action.from_uri) ) + store.dispatch(pusherActions.addQueueMetadata(tlids, action.from_uri)) - console.info('Added '+tlids.length+' URI(s) to queue') - - // still more URIs? run again in 0.8s + // still more URIs? run again in 200ms // this gives our server time to handle other requests // crude, but prevents locking the server if (remaining_uris.length > 0){ @@ -268,8 +268,12 @@ const MopidyMiddleware = (function(){ function(){ process_batch() }, - 800 + 200 ) + + // all done + } else { + store.dispatch(uiActions.removeNotification(action.type)) } }) } diff --git a/src/js/services/ui/actions.js b/src/js/services/ui/actions.js index 85ba9a82..1cf31157 100755 --- a/src/js/services/ui/actions.js +++ b/src/js/services/ui/actions.js @@ -224,14 +224,13 @@ export function createBrowserNotification( data ){ } } -export function createNotification(content, type = 'default', is_shortcut = false){ +export function createNotification(content, type = 'default', id = helpers.generateGuid()){ return { type: 'CREATE_NOTIFICATION', notification: { - id: helpers.generateGuid(), + id: id, type: type, - content: content, - is_shortcut: is_shortcut + content: content } } } diff --git a/src/js/services/ui/middleware.js b/src/js/services/ui/middleware.js index fb2fae3a..2a22f600 100755 --- a/src/js/services/ui/middleware.js +++ b/src/js/services/ui/middleware.js @@ -268,13 +268,15 @@ const UIMiddleware = (function(){ case 'CREATE_NOTIFICATION': - // start a timeout to remove this notification - var timeout = setTimeout( - function(){ - store.dispatch(uiActions.removeNotification(action.notification.id)) - }, - 3000 - ) + // start a timeout to remove this (non loading) notification + if (action.notification.type != 'loading'){ + var timeout = setTimeout( + function(){ + store.dispatch(uiActions.removeNotification(action.notification.id)) + }, + (action.notification.type == 'shortcut' ? 1000 : 3000) + ) + } next(action) break diff --git a/src/js/services/ui/reducer.js b/src/js/services/ui/reducer.js index 5950a4a1..d8a8a783 100755 --- a/src/js/services/ui/reducer.js +++ b/src/js/services/ui/reducer.js @@ -711,6 +711,7 @@ export default function reducer(ui = {}, action){ case 'CREATE_NOTIFICATION': var notifications = [...ui.notifications, action.notification] + notifications = helpers.mergeDuplicates(notifications,'id') return Object.assign({}, ui, { notifications: notifications }) case 'REMOVE_NOTIFICATION': diff --git a/src/js/views/App.js b/src/js/views/App.js index 5d51ef8d..f9b6d69e 100755 --- a/src/js/views/App.js +++ b/src/js/views/App.js @@ -106,10 +106,10 @@ class App extends React.Component{ case 32: // spacebar if (this.props.play_state == 'playing'){ this.props.mopidyActions.pause() - this.props.uiActions.createNotification(null, 'pause', true) + this.props.uiActions.createNotification('pause', 'shortcut', 'shortcut') }else{ this.props.mopidyActions.play() - this.props.uiActions.createNotification(null, 'play', true) + this.props.uiActions.createNotification('play', 'shortcut', 'shortcut') } break; diff --git a/src/js/views/Debug.js b/src/js/views/Debug.js index 0692402f..3050ea7a 100755 --- a/src/js/views/Debug.js +++ b/src/js/views/Debug.js @@ -103,7 +103,8 @@ class Debug extends React.Component{
- this.props.uiActions.createNotification('Test '+Math.floor(Date.now() / 1000))}>Create notification + this.props.uiActions.createNotification('Test notification')}>Create notification + this.props.uiActions.createNotification('Test loader','loading')}>Create loader
From 14509e6c8bafa282d60a5c4c2c81e2f5e7ad9c08 Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Wed, 5 Apr 2017 09:16:28 +1200 Subject: [PATCH 3/9] Starting cancellable loaders --- src/js/components/Notifications.js | 2 +- src/js/services/ui/actions.js | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/js/components/Notifications.js b/src/js/components/Notifications.js index c5651660..4b01bf3f 100755 --- a/src/js/components/Notifications.js +++ b/src/js/components/Notifications.js @@ -29,7 +29,7 @@ export default class Notifications extends React.Component{ case 'loading': return (
- this.props.uiActions.removeNotification(notification.id) } /> + this.props.uiActions.cancelLoading(notification.id) } /> { notification.content }
) diff --git a/src/js/services/ui/actions.js b/src/js/services/ui/actions.js index 1cf31157..7ab3ff68 100755 --- a/src/js/services/ui/actions.js +++ b/src/js/services/ui/actions.js @@ -235,7 +235,14 @@ export function createNotification(content, type = 'default', id = helpers.gener } } -export function removeNotification( id ){ +export function cancelLoading(id){ + return { + type: 'CANCEL_LOADING_'+id, + id: id + } +} + +export function removeNotification(id){ return { type: 'REMOVE_NOTIFICATION', id: id From f292e512dc96b7b4eeb71cfdb67a372a4a55e965 Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Wed, 5 Apr 2017 20:28:41 +1200 Subject: [PATCH 4/9] Creating processes --- src/js/components/DebugInfo.js | 5 +++- src/js/components/Notifications.js | 12 ++++----- src/js/services/mopidy/middleware.js | 10 ++++++- src/js/services/ui/actions.js | 36 +++++++++++++++++-------- src/js/services/ui/middleware.js | 4 +-- src/js/services/ui/reducer.js | 22 +++++++++++---- src/js/views/Debug.js | 2 +- src/scss/components/_notifications.scss | 2 +- 8 files changed, 65 insertions(+), 28 deletions(-) diff --git a/src/js/components/DebugInfo.js b/src/js/components/DebugInfo.js index b4d965fc..1716ff51 100755 --- a/src/js/components/DebugInfo.js +++ b/src/js/components/DebugInfo.js @@ -74,12 +74,15 @@ class DebugInfo extends React.Component{
Notifications: {this.props.ui.notifications ? Object.keys(this.props.ui.notifications).length : '0'}
+
+ Processes: {this.props.ui.processes ? Object.keys(this.props.ui.processes).length : '0'} +

_testMode: {window._testMode ? 'on' : 'off'}
- Touch: {helpers.isTouchDevice() ? 'yes' : 'no'} + Touch: {helpers.isTouchDevice() ? 'on' : 'off'}
{this.renderLoadQueue()} diff --git a/src/js/components/Notifications.js b/src/js/components/Notifications.js index 4b01bf3f..27e1666b 100755 --- a/src/js/components/Notifications.js +++ b/src/js/components/Notifications.js @@ -21,23 +21,23 @@ export default class Notifications extends React.Component{ switch (notification.type){ case 'shortcut': return ( -
+
) - case 'loading': + case 'process': return ( -
- this.props.uiActions.cancelLoading(notification.id) } /> +
+ this.props.uiActions.cancelProcess(notification.key) } /> { notification.content }
) default: return ( -
- this.props.uiActions.removeNotification(notification.id) } /> +
+ this.props.uiActions.removeNotification(notification.key) } /> { notification.content }
) diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index 8d7ea6ec..f8567f74 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -237,7 +237,13 @@ const MopidyMiddleware = (function(){ } } - let process_batch = function(){ + let process_batch = function(){ + + // process is not active, so abort + if (!store.getState().ui.processes || !store.getState().ui.processes[action.uri]){ + return false + } + store.dispatch(uiActions.createNotification('Adding '+remaining_uris.length+' URI(s)', 'loading', action.type)) var params = {uris: remaining_uris.splice(0,5)} @@ -274,11 +280,13 @@ const MopidyMiddleware = (function(){ // all done } else { store.dispatch(uiActions.removeNotification(action.type)) + store.dispatch(uiActions.stopProcess(action.type)) } }) } // start processing + store.dispatch(uiActions.startProcess(action.type)) process_batch() break diff --git a/src/js/services/ui/actions.js b/src/js/services/ui/actions.js index 7ab3ff68..3a8236b1 100755 --- a/src/js/services/ui/actions.js +++ b/src/js/services/ui/actions.js @@ -224,28 +224,21 @@ export function createBrowserNotification( data ){ } } -export function createNotification(content, type = 'default', id = helpers.generateGuid()){ +export function createNotification(content, type = 'default', key = helpers.generateGuid()){ return { type: 'CREATE_NOTIFICATION', notification: { - id: id, + key: key, type: type, content: content } } } -export function cancelLoading(id){ - return { - type: 'CANCEL_LOADING_'+id, - id: id - } -} - -export function removeNotification(id){ +export function removeNotification(key){ return { type: 'REMOVE_NOTIFICATION', - id: id + key: key } } @@ -268,3 +261,24 @@ export function stopLoading(key){ key: key } } + +export function startProcess(key){ + return { + type: 'START_PROCESS', + key: key + } +} + +export function cancelProcess(key){ + return { + type: 'CANCEL_PROCESS', + key: key + } +} + +export function stopProcess(key){ + return { + type: 'STOP_PROCESS', + key: key + } +} diff --git a/src/js/services/ui/middleware.js b/src/js/services/ui/middleware.js index 2a22f600..d9f05afb 100755 --- a/src/js/services/ui/middleware.js +++ b/src/js/services/ui/middleware.js @@ -269,10 +269,10 @@ const UIMiddleware = (function(){ case 'CREATE_NOTIFICATION': // start a timeout to remove this (non loading) notification - if (action.notification.type != 'loading'){ + if (action.notification.type != 'process'){ var timeout = setTimeout( function(){ - store.dispatch(uiActions.removeNotification(action.notification.id)) + store.dispatch(uiActions.removeNotification(action.notification.key)) }, (action.notification.type == 'shortcut' ? 1000 : 3000) ) diff --git a/src/js/services/ui/reducer.js b/src/js/services/ui/reducer.js index d8a8a783..42c29a1d 100755 --- a/src/js/services/ui/reducer.js +++ b/src/js/services/ui/reducer.js @@ -711,16 +711,16 @@ export default function reducer(ui = {}, action){ case 'CREATE_NOTIFICATION': var notifications = [...ui.notifications, action.notification] - notifications = helpers.mergeDuplicates(notifications,'id') + notifications = helpers.mergeDuplicates(notifications,'key') return Object.assign({}, ui, { notifications: notifications }) case 'REMOVE_NOTIFICATION': var notifications = Object.assign([], ui.notifications) - function getByID( notification ){ - return notification.id === action.id + function getByKey( notification ){ + return notification.key === action.key } - var index = notifications.findIndex(getByID) + var index = notifications.findIndex(getByKey) if( index > -1 ) notifications.splice(index, 1) return Object.assign({}, ui, { notifications: notifications }) @@ -728,7 +728,7 @@ export default function reducer(ui = {}, action){ /** - * Loader + * Loading and processes **/ case 'START_LOADING': @@ -743,6 +743,18 @@ export default function reducer(ui = {}, action){ } return Object.assign({}, ui, {load_queue: load_queue}) + case 'START_PROCESS': + var processes = Object.assign({}, (ui.processes ? ui.processes : [])) + processes[action.key] = 'active' + return Object.assign({}, ui, {processes: processes}) + + case 'STOP_PROCESS': + var processes = Object.assign({}, (ui.processes ? ui.processes : {})) + if (processes[action.key]){ + delete processes[action.key] + } + return Object.assign({}, ui, {processes: processes}) + default: return ui diff --git a/src/js/views/Debug.js b/src/js/views/Debug.js index 3050ea7a..6b716691 100755 --- a/src/js/views/Debug.js +++ b/src/js/views/Debug.js @@ -104,7 +104,7 @@ class Debug extends React.Component{
diff --git a/src/scss/components/_notifications.scss b/src/scss/components/_notifications.scss index da2ee27e..12d6d206 100755 --- a/src/scss/components/_notifications.scss +++ b/src/scss/components/_notifications.scss @@ -44,7 +44,7 @@ background: $red; } - &.loading { + &.process { position: relative; padding-right: 18px; From 27272dcd0f0d3ea73150fe51a4b19e60d35fcbd9 Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Thu, 6 Apr 2017 09:01:53 +1200 Subject: [PATCH 5/9] Cancellable processes --- src/js/components/Notifications.js | 43 +++++++++++++++++++++------- src/js/services/mopidy/middleware.js | 16 +++++++---- src/js/services/ui/actions.js | 5 ++-- src/js/services/ui/middleware.js | 16 +++++------ src/js/services/ui/reducer.js | 18 +++++++++++- src/js/views/App.js | 2 ++ src/js/views/Debug.js | 3 +- 7 files changed, 74 insertions(+), 29 deletions(-) diff --git a/src/js/components/Notifications.js b/src/js/components/Notifications.js index 27e1666b..d3f23002 100755 --- a/src/js/components/Notifications.js +++ b/src/js/components/Notifications.js @@ -11,9 +11,6 @@ export default class Notifications extends React.Component{ renderNotifications(){ if (!this.props.notifications || this.props.notifications.length <= 0) return null - // we only care about the last notification - var notification = this.props.notifications[this.props.notifications.length-1] - return ( { @@ -26,14 +23,6 @@ export default class Notifications extends React.Component{
) - case 'process': - return ( -
- this.props.uiActions.cancelProcess(notification.key) } /> - { notification.content } -
- ) - default: return (
@@ -48,6 +37,37 @@ export default class Notifications extends React.Component{ ) } + renderProcesses(){ + if (!this.props.processes || this.props.processes.length <= 0) return null + var processes = this.props.processes + var items = [] + + for (var key in processes){ + if (processes.hasOwnProperty(key)){ + if (processes[key].cancelling){ + items.push( +
+ Cancelling +
+ ) + } else { + items.push( +
+ this.props.uiActions.cancelProcess(key) } /> + { processes[key].content } +
+ ) + } + } + } + + return ( + + {items} + + ) + } + // do we want the loading of everything to be displayed? // not likely... renderLoader(){ @@ -79,6 +99,7 @@ export default class Notifications extends React.Component{ return (
{this.renderNotifications()} + {this.renderProcesses()}
) } diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index f8567f74..1e59767c 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -239,12 +239,19 @@ const MopidyMiddleware = (function(){ let process_batch = function(){ - // process is not active, so abort - if (!store.getState().ui.processes || !store.getState().ui.processes[action.uri]){ + // process is not running + if (store.getState().ui.processes && store.getState().ui.processes[action.type]){ + if (store.getState().ui.processes[action.type].cancelling){ + // recognise as cancelled + store.dispatch(uiActions.stopProcess(action.type)) + return false + } + } else { return false } - store.dispatch(uiActions.createNotification('Adding '+remaining_uris.length+' URI(s)', 'loading', action.type)) + // update our process details + store.dispatch(uiActions.startProcess(action.type, 'Adding '+remaining_uris.length+' URI(s)')) var params = {uris: remaining_uris.splice(0,5)} if (action.next && current_track_index > -1){ @@ -279,14 +286,13 @@ const MopidyMiddleware = (function(){ // all done } else { - store.dispatch(uiActions.removeNotification(action.type)) store.dispatch(uiActions.stopProcess(action.type)) } }) } // start processing - store.dispatch(uiActions.startProcess(action.type)) + store.dispatch(uiActions.startProcess(action.type, 'Adding '+remaining_uris.length+' URI(s)')) process_batch() break diff --git a/src/js/services/ui/actions.js b/src/js/services/ui/actions.js index 3a8236b1..4170aee0 100755 --- a/src/js/services/ui/actions.js +++ b/src/js/services/ui/actions.js @@ -262,10 +262,11 @@ export function stopLoading(key){ } } -export function startProcess(key){ +export function startProcess(key,content){ return { type: 'START_PROCESS', - key: key + key: key, + content: content } } diff --git a/src/js/services/ui/middleware.js b/src/js/services/ui/middleware.js index d9f05afb..9ea43811 100755 --- a/src/js/services/ui/middleware.js +++ b/src/js/services/ui/middleware.js @@ -268,15 +268,13 @@ const UIMiddleware = (function(){ case 'CREATE_NOTIFICATION': - // start a timeout to remove this (non loading) notification - if (action.notification.type != 'process'){ - var timeout = setTimeout( - function(){ - store.dispatch(uiActions.removeNotification(action.notification.key)) - }, - (action.notification.type == 'shortcut' ? 1000 : 3000) - ) - } + // start a timeout to remove this notification + var timeout = setTimeout( + function(){ + store.dispatch(uiActions.removeNotification(action.notification.key)) + }, + (action.notification.type == 'shortcut' ? 1000 : 3000) + ) next(action) break diff --git a/src/js/services/ui/reducer.js b/src/js/services/ui/reducer.js index 42c29a1d..28f9804d 100755 --- a/src/js/services/ui/reducer.js +++ b/src/js/services/ui/reducer.js @@ -745,7 +745,23 @@ export default function reducer(ui = {}, action){ case 'START_PROCESS': var processes = Object.assign({}, (ui.processes ? ui.processes : [])) - processes[action.key] = 'active' + processes[action.key] = { + key: action.key, + content: action.content + } + return Object.assign({}, ui, {processes: processes}) + + case 'CANCEL_PROCESS': + var processes = Object.assign({}, (ui.processes ? ui.processes : {})) + if (processes[action.key]){ + processes[action.key] = Object.assign( + {}, + processes[action.key], + { + cancelling: true + } + ) + } return Object.assign({}, ui, {processes: processes}) case 'STOP_PROCESS': diff --git a/src/js/views/App.js b/src/js/views/App.js index f9b6d69e..541c97a2 100755 --- a/src/js/views/App.js +++ b/src/js/views/App.js @@ -158,6 +158,7 @@ class App extends React.Component{ {this.props.debug_info ? : null}
@@ -174,6 +175,7 @@ class App extends React.Component{ const mapStateToProps = (state, ownProps) => { return { notifications: (state.ui.notifications ? state.ui.notifications : []), + processes: (state.ui.processes ? state.ui.processes : {}), load_queue: (state.ui.load_queue ? state.ui.load_queue : {}), mopidy_connected: state.mopidy.connected, spotify_authorized: state.spotify.authorized, diff --git a/src/js/views/Debug.js b/src/js/views/Debug.js index 6b716691..816927c6 100755 --- a/src/js/views/Debug.js +++ b/src/js/views/Debug.js @@ -104,7 +104,8 @@ class Debug extends React.Component{
From e143f0e71addaa72dadfc8e1482b2a4788560af0 Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Thu, 6 Apr 2017 18:13:27 +1200 Subject: [PATCH 6/9] Spacing --- mopidy_iris/handlers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mopidy_iris/handlers.py b/mopidy_iris/handlers.py index 9529ec40..409abaf8 100755 --- a/mopidy_iris/handlers.py +++ b/mopidy_iris/handlers.py @@ -91,13 +91,15 @@ class WebsocketHandler(tornado.websocket.WebSocketHandler): mem.iris.send_message(self.connection_id, response) else: response = { - 'error': 'Method "'+message['method']+'" does not exist', + 'status': 0, + 'message': 'Method "'+message['method']+'" does not exist', 'request_id': request_id } mem.iris.send_message(self.connection_id, response) else: response = { - 'error': 'Method key missing from request', + 'status': 0, + 'message': 'Method key missing from request', 'request_id': request_id } mem.iris.send_message(self.connection_id, response) From 62ed2170212c106e77b6ecacd2532757a1823fdf Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Thu, 6 Apr 2017 20:50:50 +1200 Subject: [PATCH 7/9] Playlist description links to users --- src/js/services/spotify/actions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/js/services/spotify/actions.js b/src/js/services/spotify/actions.js index ba3a2ff3..fb2ea24c 100755 --- a/src/js/services/spotify/actions.js +++ b/src/js/services/spotify/actions.js @@ -919,6 +919,7 @@ export function getPlaylist( uri ){ description = response.description description = description.split(' Date: Thu, 6 Apr 2017 21:24:21 +1200 Subject: [PATCH 8/9] Change of plan - use playlist uri and load all tracks on demand --- src/js/services/mopidy/middleware.js | 20 +++++++++++++++++++- src/js/views/Playlist.js | 3 +-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index 1e59767c..3155e213 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -304,8 +304,26 @@ const MopidyMiddleware = (function(){ store.dispatch( pusherActions.stopRadio() ) } + var first_uri = action.uris[0] + + // spotify playlist and albums are handled differently + if (helpers.uriSource(first_uri) == 'spotify'){ + switch (helpers.uriType(first_uri)){ + case 'playlist': + // TODO + // trigger loading of all the playlist tracks + // once loaded, re-run play uris + // maybe create new play_album play_playlist actions to handle + // as we'd then reuse play_uris for the loaded track uris + break + + case 'album': + break + } + } + // add our first track - instruct( socket, store, 'tracklist.add', { uri: action.uris[0], at_position: 0 } ) + instruct( socket, store, 'tracklist.add', { uri: first_uri, at_position: 0 } ) .then( response => { // treat empty response as a failed lookup diff --git a/src/js/views/Playlist.js b/src/js/views/Playlist.js index 121e59fa..e023d27c 100755 --- a/src/js/views/Playlist.js +++ b/src/js/views/Playlist.js @@ -70,8 +70,7 @@ class Playlist extends React.Component{ play(){ ReactGA.event({ category: 'Playlist', action: 'Play', label: this.props.playlist.uri }) - var tracks_uris = helpers.asURIs(this.props.playlist.tracks) - this.props.mopidyActions.playURIs(tracks_uris, this.props.params.uri) + this.props.mopidyActions.playURIs([this.props.playlist.uri], this.props.params.uri) } follow(){ From 581685378774307714469ed680e1f0d6e80e5aed Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Fri, 7 Apr 2017 10:33:54 +1200 Subject: [PATCH 9/9] Playlists have their own play() function; If HTTP API, then preload and then enqueue in batches --- src/js/components/ContextMenu.js | 15 +++- src/js/services/mopidy/actions.js | 14 ++++ src/js/services/mopidy/middleware.js | 103 +++++++++++++++++---------- src/js/services/spotify/actions.js | 45 ++++++++++-- src/js/views/Playlist.js | 2 +- 5 files changed, 137 insertions(+), 42 deletions(-) diff --git a/src/js/components/ContextMenu.js b/src/js/components/ContextMenu.js index 56e55114..fe2a9391 100755 --- a/src/js/components/ContextMenu.js +++ b/src/js/components/ContextMenu.js @@ -169,6 +169,11 @@ class ContextMenu extends React.Component{ this.props.mopidyActions.playURIs(this.props.menu.uris, this.props.menu.tracklist_uri) } + playPlaylist(e){ + this.props.uiActions.hideContextMenu() + this.props.mopidyActions.playPlaylist(this.props.menu.uris[0]) + } + addToQueue(e, next = false){ this.props.uiActions.hideContextMenu() this.props.mopidyActions.enqueueURIs(this.props.menu.uris, this.props.menu.tracklist_uri, next) @@ -326,6 +331,14 @@ class ContextMenu extends React.Component{
) + var play_playlist = ( + + this.playPlaylist(e)}> + Play + + + ) + var play_queue_item = ( this.playQueueItem(e)}> @@ -446,7 +459,7 @@ class ContextMenu extends React.Component{ case 'playlist': return (
- {play_uris} + {play_playlist} {this.canBeInLibrary() ? toggle_in_library : null} {context.source == 'spotify' ? go_to_user : null} {copy_uris} diff --git a/src/js/services/mopidy/actions.js b/src/js/services/mopidy/actions.js index 847da96f..ab78b5cb 100755 --- a/src/js/services/mopidy/actions.js +++ b/src/js/services/mopidy/actions.js @@ -67,6 +67,20 @@ export function enqueueURIs( uris, from_uri = null, next = false, at_position = } } +export function playPlaylist(uri){ + return { + type: 'MOPIDY_PLAY_PLAYLIST', + uri: uri + } +} + +export function playAlbum(uri){ + return { + type: 'MOPIDY_PLAY_ALBUM', + uri: uri + } +} + export function removeTracks( tlids ){ return { type: 'MOPIDY_INSTRUCT', diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index 3155e213..92da4354 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -4,8 +4,9 @@ import * as helpers from '../../helpers' var mopidyActions = require('./actions.js') var uiActions = require('../ui/actions.js') -var lastfmActions = require('../lastfm/actions.js') +var spotifyActions = require('../spotify/actions.js') var pusherActions = require('../pusher/actions.js') +var lastfmActions = require('../lastfm/actions.js') const MopidyMiddleware = (function(){ @@ -219,7 +220,42 @@ const MopidyMiddleware = (function(){ icon: (store.getState().ui.current_track ? helpers.getTrackIcon( store.getState().ui.current_track ) : false) } store.dispatch( pusherActions.deliverBroadcast(data) ) - break; + break + + case 'MOPIDY_PLAY_PLAYLIST': + if (helpers.uriSource(action.uri) == 'spotify'){ + + // playlist already in index + if (store.getState().ui.playlists.hasOwnProperty(action.uri)){ + + // make sure we didn't get this playlist from Mopidy-Spotify + // if we did, we'd have a cached version on server so no need to fetch + if (!store.getState().ui.playlists[action.uri].is_mopidy){ + store.dispatch(uiActions.startProcess('MOPIDY_ENQUEUE_URIS', 'Fetching tracks')) + store.dispatch(spotifyActions.getAllPlaylistTracks(action.uri)) + break + } + + // not loaded, so we need to fetch + } else { + store.dispatch(uiActions.startProcess('MOPIDY_ENQUEUE_URIS', 'Fetching tracks')) + store.dispatch(spotifyActions.getAllPlaylistTracks(action.uri)) + break + } + } + + // default to load it as per usual + store.dispatch(mopidyActions.playURIs([action.uri], action.uri)) + + break + + case 'SPOTIFY_ALL_PLAYLIST_TRACKS_LOADED_FOR_PLAYING': + var uris = [] + for (var i = 0; i < action.tracks.length; i++){ + uris.push(action.tracks[i].track.uri) + } + store.dispatch(mopidyActions.playURIs(uris, action.uri)) + break case 'MOPIDY_ENQUEUE_URIS': var uris_added = 0 @@ -273,7 +309,7 @@ const MopidyMiddleware = (function(){ } store.dispatch(pusherActions.addQueueMetadata(tlids, action.from_uri)) - // still more URIs? run again in 200ms + // still more URIs? run again in 100ms // this gives our server time to handle other requests // crude, but prevents locking the server if (remaining_uris.length > 0){ @@ -281,7 +317,7 @@ const MopidyMiddleware = (function(){ function(){ process_batch() }, - 200 + 100 ) // all done @@ -306,45 +342,35 @@ const MopidyMiddleware = (function(){ var first_uri = action.uris[0] - // spotify playlist and albums are handled differently - if (helpers.uriSource(first_uri) == 'spotify'){ - switch (helpers.uriType(first_uri)){ - case 'playlist': - // TODO - // trigger loading of all the playlist tracks - // once loaded, re-run play uris - // maybe create new play_album play_playlist actions to handle - // as we'd then reuse play_uris for the loaded track uris - break - - case 'album': - break - } - } - // add our first track - instruct( socket, store, 'tracklist.add', { uri: first_uri, at_position: 0 } ) - .then( response => { + instruct(socket, store, 'tracklist.add', { uri: first_uri, at_position: 0 }) + .then(response => { - // treat empty response as a failed lookup - if( !response || response.length <= 0 ){ - store.dispatch( uiActions.createNotification('Failed to load URI(s)', 'bad') ) - console.error(action) - }else{ - // play it - store.dispatch( mopidyActions.changeTrack( response[0].tlid ) ); + // play it (only if we got a successful lookup) + if (response.length > 0){ + store.dispatch(mopidyActions.changeTrack(response[0].tlid)); var tlids = [] for (var i = 0; i < response.length; i++){ tlids.push(response[i].tlid) } - store.dispatch( pusherActions.addQueueMetadata(tlids, action.from_uri) ) + store.dispatch(pusherActions.addQueueMetadata(tlids, action.from_uri)) + } else { + store.dispatch(uiActions.createNotification('Failed to add some URI(s)', 'bad')) + } - // add the rest of our uris (if any) - action.uris.shift(); - if( action.uris.length > 0 ){ - store.dispatch( mopidyActions.enqueueURIs( action.uris, action.from_uri, 1 ) ) - } + // add the rest of our uris (if any) + action.uris.shift(); + if( action.uris.length > 0 ){ + + // wait 100ms so the server can trigger track_changed etc + // this means our UI feels snappier as the first track shows up quickly + setTimeout( + function(){ + store.dispatch(mopidyActions.enqueueURIs( action.uris, action.from_uri, 1 )) + }, + 100 + ) } }) break; @@ -490,7 +516,8 @@ const MopidyMiddleware = (function(){ type: 'playlist', name: response.name, uri: response.uri, - source: (source == 'spotify' ? 'local' : source), + source: source, + is_mopidy: true, last_modified: response.last_modified, tracks_total: ( response.tracks ? response.tracks.length : 0 ) } @@ -514,6 +541,7 @@ const MopidyMiddleware = (function(){ response, { type: 'playlist', + is_mopidy: true, tracks: ( response.tracks ? response.tracks : [] ), tracks_total: ( response.tracks ? response.tracks.length : [] ) } @@ -738,6 +766,7 @@ const MopidyMiddleware = (function(){ var album = Object.assign( {}, { + is_mopidy: true, artists: response[uri][0].artists, tracks: response[uri], tracks_total: response[uri].length @@ -766,6 +795,7 @@ const MopidyMiddleware = (function(){ { images: [] }, response[0].album, { + is_mopidy: true, artists: response[0].artists, tracks: response, tracks_total: response.length @@ -869,6 +899,7 @@ const MopidyMiddleware = (function(){ {}, (response ? response[0].artists[0] : {}), { + is_mopidy: true, albums_uris: helpers.asURIs(albums), tracks: response.slice(0,10) } diff --git a/src/js/services/spotify/actions.js b/src/js/services/spotify/actions.js index fb2ea24c..2d215e7b 100755 --- a/src/js/services/spotify/actions.js +++ b/src/js/services/spotify/actions.js @@ -890,7 +890,7 @@ export function createPlaylist( name, is_public ){ } } -export function savePlaylist( uri, name, is_public ){ +export function savePlaylist(uri, name, is_public){ return (dispatch, getState) => { sendRequest( dispatch, getState, 'users/'+ getState().spotify.me.id +'/playlists/'+ helpers.getFromUri('playlistid',uri), 'PUT', { name: name, public: is_public } ) @@ -906,7 +906,7 @@ export function savePlaylist( uri, name, is_public ){ } } -export function getPlaylist( uri ){ +export function getPlaylist(uri){ return (dispatch, getState) => { // get the main playlist object @@ -943,7 +943,44 @@ export function getPlaylist( uri ){ } } -function loadNextPlaylistsBatch( dispatch, getState, playlists, lastResponse ){ +/** + * Get all tracks for a playlist + * + * Recursively get .next until we have all tracks + **/ +function loadNextPlaylistTracksBatch(dispatch, getState, uri, tracks, lastResponse){ + if( lastResponse.next ){ + sendRequest(dispatch, getState, lastResponse.next) + .then( response => { + tracks = [...tracks, ...response.items] + loadNextPlaylistTracksBatch(dispatch, getState, uri, tracks, response) + }); + }else{ + dispatch({ + type: 'SPOTIFY_ALL_PLAYLIST_TRACKS_LOADED_FOR_PLAYING', + uri: uri, + tracks: tracks + }); + } +} + +export function getAllPlaylistTracks(uri){ + return (dispatch, getState) => { + sendRequest(dispatch, getState, 'users/'+ helpers.getFromUri('userid',uri) +'/playlists/'+ helpers.getFromUri('playlistid',uri) +'/tracks?market='+getState().spotify.country) + .then( response => { + loadNextPlaylistTracksBatch(dispatch, getState, uri, response.items, response) + }); + } +} + + +/** + * Get all of our playlists in one foul swoop + * + * Recursively gets playlists until no more .next value. Fails when user has + * a lot of playlists, and we hit the API limits + **/ +function loadNextPlaylistsBatch(dispatch, getState, playlists, lastResponse){ if( lastResponse.next ){ sendRequest( dispatch, getState, lastResponse.next ) .then( response => { @@ -967,7 +1004,7 @@ export function getAllLibraryPlaylists(){ } } -export function toggleFollowingPlaylist( uri, method ){ +export function toggleFollowingPlaylist(uri, method){ if( method == 'PUT' ) var new_state = 1 if( method == 'DELETE' ) var new_state = 0 diff --git a/src/js/views/Playlist.js b/src/js/views/Playlist.js index e023d27c..dc7489d0 100755 --- a/src/js/views/Playlist.js +++ b/src/js/views/Playlist.js @@ -70,7 +70,7 @@ class Playlist extends React.Component{ play(){ ReactGA.event({ category: 'Playlist', action: 'Play', label: this.props.playlist.uri }) - this.props.mopidyActions.playURIs([this.props.playlist.uri], this.props.params.uri) + this.props.mopidyActions.playPlaylist(this.props.playlist.uri) } follow(){