Upgrading process calls; Tracks library now works like aother asset libraries (huge libraries may kill react-persist?)

This commit is contained in:
James Barnsley
2020-10-23 23:11:24 +13:00
parent eca594e9e5
commit 5724f102cf
19 changed files with 1571 additions and 1029 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -106,7 +106,7 @@
// Release details
// These are automatically injected to built HTML
var build = "1603178804";
var build = "1603447492";
var version = "3.53.0";
// Construct the script tag

View File

@ -32,12 +32,12 @@ export default memo((props) => {
const {
body,
loading,
finished = false,
finished,
mini,
lazy,
white,
className = '',
progress = null,
progress,
} = props;
if (!loading && !finished) {
@ -85,7 +85,7 @@ export default memo((props) => {
);
}
if (progress) {
if (progress !== undefined) {
classNameString += ' loader--progress';
return (
<div className={classNameString}>

View File

@ -38,6 +38,7 @@ actions:
confirm: Are you sure?
delete: Delete
remove: Remove
cancel: Cancel
common:
popularity: Popularity
name: Name
@ -117,6 +118,7 @@ services:
title: Mopidy
local: Local
adding_uris: 'Adding %{count} URI(s)'
adding_uris_broadcast: '%{username} is adding %{count} URI(s)'
searching: 'Searching %{provider} %{type}'
searching_providers: 'Searching %{count} Mopidy providers'
loading_albums: 'Loading %{count} local albums'

View File

@ -82,6 +82,10 @@ const ensureLoaded = ({
if (item) {
if (getMissingDependents(item).length === 0) {
console.info(`"${uri}" and all dependents already in index`);
// TODO
// When an item is in the index, we assume the dependents are loaded as well, which in the
// case of local artists is not true. Ie a local artist's albums are not auto-loaded.
store.dispatch(uiActions.stopLoading(uri));
coreActions.loadItems(getDependentUris(item));
return;
@ -373,6 +377,7 @@ const CoreMiddleware = (function () {
switch (uriSource(key)) {
case 'spotify':
console.log("GETTING", key)
store.dispatch(spotifyActions.getPlaylist(key));
break;
case 'm3u':

View File

@ -26,9 +26,7 @@ const GoogleMiddleware = (function () {
return (store) => (next) => (action) => {
switch (action.type) {
case 'GOOGLE_GET_LIBRARY_ALBUMS': {
store.dispatch(
uiActions.updateProcess(action.type, { notification: false }),
);
store.dispatch(uiActions.startProcess(action.type, { notification: false }) );
request(
store,
@ -38,7 +36,7 @@ const GoogleMiddleware = (function () {
const allUris = arrayOf('uri', browseResponse);
store.dispatch(uiActions.updateProcess(
'GOOGLE_GET_LIBRARY_ALBUMS',
action.type,
{
remaining: allUris.length,
total: allUris.length,
@ -83,7 +81,7 @@ const GoogleMiddleware = (function () {
store.dispatch(uiActions.processFinished(action.type));
store.dispatch(coreActions.libraryLoaded({
uri: 'google:library:albums',
items_uris: arrayOf('uri', allUris),
items_uris: arrayOf('uri', browseResponse),
}));
}
};
@ -95,7 +93,7 @@ const GoogleMiddleware = (function () {
}
case 'GOOGLE_GET_LIBRARY_ARTISTS': {
store.dispatch(uiActions.updateProcess(action.type, { notification: false }));
store.dispatch(uiActions.startProcess(action.type, { notification: false }));
request(
store,
@ -149,7 +147,7 @@ const GoogleMiddleware = (function () {
store.dispatch(uiActions.processFinished(action.type));
store.dispatch(coreActions.libraryLoaded({
uri: 'google:library:artists',
items_uris: arrayOf('uri', allUris),
items_uris: arrayOf('uri', browseResponse),
}));
}
};

View File

@ -473,9 +473,17 @@ export function getLibraryArtists(uri = null) {
};
}
export function clearLibraryArtists() {
export function getLibraryAlbums(uri = null) {
return {
type: 'MOPIDY_CLEAR_LIBRARY_ARTISTS',
type: 'MOPIDY_GET_LIBRARY_ALBUMS',
uri,
};
}
export function getLibraryTracks(uri = null) {
return {
type: 'MOPIDY_GET_LIBRARY_TRACKS',
uri,
};
}
@ -495,39 +503,6 @@ export function getAlbum(uri, options) {
};
}
export function getAlbums(uris, processor = null) {
return {
type: 'MOPIDY_GET_ALBUMS',
uris,
processor,
};
}
export function getLibraryAlbums(uri = null) {
return {
type: 'MOPIDY_GET_LIBRARY_ALBUMS',
uri,
};
}
export function clearLibraryAlbums() {
return {
type: 'MOPIDY_CLEAR_LIBRARY_ALBUMS',
};
}
export function runProcessor(processor) {
return {
type: processor,
};
}
export function cancelProcessor(processor) {
return {
type: `${processor}_CANCEL`,
};
}
export function view_getRandomTracks(limit = 100) {
return {
type: 'VIEW__GET_RANDOM_TRACKS',
@ -535,11 +510,6 @@ export function view_getRandomTracks(limit = 100) {
};
}
/**
* Searching
* */
export function clearSearchResults() {
return {
type: 'MOPIDY_CLEAR_SEARCH_RESULTS',
@ -555,11 +525,6 @@ export function getSearchResults(query, limit = 100, uri_schemes) {
};
}
/**
* Other general actions
* */
export function getQueueHistory() {
return {
type: 'MOPIDY_GET_QUEUE_HISTORY',

View File

@ -2,7 +2,7 @@
import ReactGA from 'react-ga';
import Mopidy from 'mopidy';
import { sha256 } from 'js-sha256';
import { sampleSize, compact } from 'lodash';
import { sampleSize, compact, chunk } from 'lodash';
import { i18n } from '../../locale';
import {
generateGuid,
@ -257,7 +257,8 @@ const MopidyMiddleware = (function () {
method = 'library.search',
data,
} = queue.shift();
const processor = store.getState().ui.processes.MOPIDY_GET_SEARCH_RESULTS;
const processKey = 'MOPIDY_GET_SEARCH_RESULTS';
const processor = store.getState().ui.processes[processKey];
if (processor && processor.status === 'cancelling') {
store.dispatch(uiActions.processCancelled('MOPIDY_GET_SEARCH_RESULTS'));
@ -265,7 +266,7 @@ const MopidyMiddleware = (function () {
}
store.dispatch(uiActions.updateProcess(
'MOPIDY_GET_SEARCH_RESULTS',
processKey,
i18n(
'services.mopidy.searching',
{
@ -369,9 +370,7 @@ const MopidyMiddleware = (function () {
if (queue.length) {
processSearchQueue(store, queue);
} else {
store.dispatch(uiActions.processFinished(
'MOPIDY_GET_SEARCH_RESULTS',
));
store.dispatch(uiActions.processFinished(processKey));
}
},
);
@ -862,11 +861,42 @@ const MopidyMiddleware = (function () {
break;
}
let remaining = action.uris.length;
let at_position = action.at_position;
// Uris are to go immediately after the currently-playing track (which could be paused)
if (action.play_next) {
const {
current_track,
queue,
} = store.getState().core;
if (current_track) {
for (let i = 0; i < queue.length; i += 1) {
if (queue[i].tlid === current_track.tlid) {
at_position = i + 1;
break;
}
}
}
}
store.dispatch(uiActions.startProcess(
action.type,
{
content: i18n('services.mopidy.adding_uris', { count: remaining }),
remaining,
total: remaining,
},
));
store.dispatch(pusherActions.deliverBroadcast(
'notification',
{
notification: {
content: `${store.getState().pusher.username} is adding ${action.uris.length} URIs to queue`,
content: i18n(
'services.mopidy.adding_uris_broadcast',
{ username: store.getState().pusher.username, count: remaining },
),
icon: (
store.getState().core.current_track
? getTrackIcon(store.getState().core.current_track, store.getState().core)
@ -876,125 +906,65 @@ const MopidyMiddleware = (function () {
},
));
const uris = Object.assign([], action.uris);
const batches = [];
const batch_size = 5;
while (uris.length > 0) {
batches.push({
uris: uris.splice(0, batch_size),
at_position: action.at_position,
play_next: action.play_next,
offset: action.offset + (batch_size * batches.length),
from_uri: action.from_uri,
});
}
// Prepare our URIS into batches. This allows performance gain of several URIs at once
// (which is a blocking request), but not so many that it locks the Mopidy server.
// It allows a window of opportunity for other requests to complete before we proceed to
// the next batch.
const batchSize = 5;
const batches = chunk(action.uris, batchSize);
next({
...action,
batches,
});
store.dispatch(uiActions.startProcess(
'MOPIDY_ENQUEUE_URIS_PROCESSOR',
i18n('services.mopidy.adding_uris', { count: action.uris.length }),
{
batches,
remaining: action.uris.length,
total: action.uris.length,
},
));
break;
}
case 'MOPIDY_ENQUEUE_URIS_PROCESSOR': {
const last_run = store.getState().ui.processes.MOPIDY_ENQUEUE_URIS_PROCESSOR;
if (last_run && last_run.status == 'cancelling') {
store.dispatch(uiActions.processCancelled('MOPIDY_ENQUEUE_URIS_PROCESSOR'));
return;
} if (action.data.batches && action.data.batches.length > 0) {
var batches = Object.assign([], action.data.batches);
var batch = batches[0];
let total_uris = 0;
for (var i = 0; i < batches.length; i++) {
total_uris += batches[i].uris.length;
}
batches.shift();
store.dispatch(uiActions.updateProcess(
'MOPIDY_ENQUEUE_URIS_PROCESSOR',
`Adding ${total_uris} URI(s)`,
{
remaining: total_uris,
},
));
// no batches means we're done here
} else {
store.dispatch(uiActions.processFinished('MOPIDY_ENQUEUE_URIS_PROCESSOR'));
break;
}
const {
current_track,
queue,
} = store.getState().core;
let current_track_index = -1;
if (current_track) {
for (var i = 0; i < queue.length; i++) {
if (queue[i].tlid == current_track.tlid) {
current_track_index = i;
break;
}
}
}
var params = { uris: batch.uris };
// Play this batch next
if (batch.play_next) {
// Make sure we're playing something first
if (current_track_index > -1) {
params.at_position = current_track_index + batch.offset + 1;
// Default to top of queue if we're not playing
} else {
params.at_position = 0 + batch.offset;
// This is our process iterator
const run = () => {
const processor = store.getState().ui.processes[action.type];
if (processor && processor.status === 'cancelling') {
store.dispatch(uiActions.processCancelled(action.type));
return;
}
// A specific position has been defined
// NOTE: This is likely to be wrong as the original action is unaware of batches or other client requests
} else if (batch.at_position) {
params.at_position = batch.at_position + batch.offset;
}
const uris = batches.shift();
request(store, 'tracklist.add', params)
.then(
(response) => {
// add metadata to queue
const tlids = [];
for (let i = 0; i < response.length; i++) {
tlids.push(response[i].tlid);
}
store.dispatch(pusherActions.addQueueMetadata(tlids, batch.from_uri));
request(store, 'tracklist.add', { uris, at_position })
.then(
(response) => {
const tlids = response.map((track) => track.tlid);
store.dispatch(pusherActions.addQueueMetadata(tlids, action.from_uri));
// Re-run the batch checker in 100ms. This allows a small window for other
// server requests before our next batch. It's a little crude but it means the server isn't
// locked until we're completely done.
setTimeout(
() => {
store.dispatch(uiActions.runProcess(action.type, { batches }));
},
100,
);
},
(error) => {
store.dispatch(coreActions.handleException(
`Mopidy: ${error.message ? error.message : 'Adding tracks failed'}`,
error,
));
},
);
// Re-run the batch checker in 100ms. This allows a small window for other server
// requests before our next batch. A little crude but it means the server isn't
// locked until we're completely done.
setTimeout(
() => {
if (!batches.length) {
store.dispatch(uiActions.processFinished(action.type));
} else {
remaining -= uris.length;
if (at_position !== null) at_position += uris.length;
store.dispatch(
uiActions.updateProcess(
action.type,
{
remaining,
content: i18n('services.mopidy.adding_uris', { count: remaining }),
},
),
);
run();
}
},
100,
);
},
(error) => {
store.dispatch(uiActions.processFinished(action.type));
store.dispatch(coreActions.handleException(
`Mopidy: ${error.message || 'Adding tracks failed'}`,
error,
));
},
);
};
run(); // Kick off the loop
break;
}
@ -1724,14 +1694,8 @@ const MopidyMiddleware = (function () {
* My Music libraries
*/
case 'MOPIDY_GET_LIBRARY_ARTISTS':
store.dispatch(
uiActions.startProcess(
'MOPIDY_GET_LIBRARY_ARTISTS',
{
notification: false,
},
),
);
store.dispatch(uiActions.startProcess(action.type, { notification: false }));
request(store, 'library.browse', { uri: store.getState().mopidy.library_artists_uri })
.then((raw_response) => {
if (raw_response.length <= 0) return;
@ -1748,19 +1712,12 @@ const MopidyMiddleware = (function () {
uri: 'mopidy:library:artists',
items_uris: arrayOf('uri', response),
}));
store.dispatch(uiActions.processFinished('MOPIDY_GET_LIBRARY_ARTISTS'));
store.dispatch(uiActions.processFinished(action.type));
});
break;
case 'MOPIDY_GET_LIBRARY_PLAYLISTS': {
store.dispatch(
uiActions.startProcess(
'MOPIDY_GET_LIBRARY_PLAYLISTS',
{
notification: false,
},
),
);
store.dispatch(uiActions.startProcess(action.type, { notification: false }));
request(store, 'playlists.asList')
.then((listResponse) => {
@ -1799,7 +1756,7 @@ const MopidyMiddleware = (function () {
store.dispatch(
uiActions.updateProcess(
'MOPIDY_GET_LIBRARY_PLAYLISTS',
action.type,
{
remaining: playlist_uris.length - index - 1,
},
@ -1812,35 +1769,28 @@ const MopidyMiddleware = (function () {
uri: 'mopidy:library:playlists',
items_uris: arrayOf('uri', libraryPlaylists),
}));
store.dispatch(uiActions.processFinished('MOPIDY_GET_LIBRARY_PLAYLISTS'));
store.dispatch(uiActions.processFinished(action.type));
}
});
});
} else {
store.dispatch(uiActions.stopLoading('mopidy:library:playlists'));
store.dispatch(uiActions.processFinished('MOPIDY_GET_LIBRARY_PLAYLISTS'));
store.dispatch(uiActions.processFinished(action.type));
}
});
break;
}
case 'MOPIDY_GET_LIBRARY_ALBUMS':
store.dispatch(
uiActions.startProcess(
'MOPIDY_GET_LIBRARY_ALBUMS',
{
notification: false,
},
),
);
store.dispatch(uiActions.startProcess(action.type, { notification: false }));
request(store, 'library.browse', { uri: store.getState().mopidy.library_albums_uri })
.then((response) => {
const uris = arrayOf('uri', response);
.then((browseResponse) => {
const uris = arrayOf('uri', browseResponse);
store.dispatch(
uiActions.updateProcess(
'MOPIDY_GET_LIBRARY_ALBUMS',
action.type,
{
total: uris.length,
remaining: uris.length,
@ -1849,29 +1799,82 @@ const MopidyMiddleware = (function () {
);
request(store, 'library.lookup', { uris })
.then((response) => {
const libraryAlbums = indexToArray(response).map((tracks) => ({
.then((lookupResponse) => {
const libraryAlbums = indexToArray(lookupResponse).map((tracks) => ({
artists: tracks[0].artists ? formatArtists(tracks[0].artists) : null,
tracks: formatTracks(tracks),
last_modified: tracks[0].last_modified,
...formatAlbum(tracks[0].album),
}));
console.log('LOADED', response);
store.dispatch(coreActions.itemsLoaded(libraryAlbums));
store.dispatch(coreActions.libraryLoaded({
uri: 'mopidy:library:albums',
items_uris: arrayOf('uri', libraryAlbums),
}));
store.dispatch(uiActions.processFinished('MOPIDY_GET_LIBRARY_ALBUMS'));
store.dispatch(uiActions.processFinished(action.type));
});
});
break;
// This action is irrelevant to us, pass it on to the next middleware
case 'MOPIDY_GET_LIBRARY_TRACKS':
store.dispatch(uiActions.startProcess(action.type, { notification: false }));
request(store, 'library.browse', { uri: 'local:directory?type=track' })
.then((browseResponse) => {
const allUris = arrayOf('uri', browseResponse);
store.dispatch(uiActions.updateProcess(
action.type,
{
remaining: allUris.length,
total: allUris.length,
},
));
const run = () => {
if (allUris.length) {
const uris = allUris.splice(0, 100);
const processor = store.getState().ui.processes[action.type];
if (processor && processor.status === 'cancelling') {
store.dispatch(uiActions.processCancelled(action.type));
store.dispatch(uiActions.stopLoading('mopidy:library:tracks'));
return;
}
store.dispatch(uiActions.updateProcess(action.type, { remaining: allUris.length }));
request(store, 'library.lookup', { uris })
.then(
(lookupResponse) => {
const libraryItems = compact(
indexToArray(lookupResponse).map(
(results) => (results.length ? formatTrack(results[0]) : null),
),
);
if (libraryItems.length) {
store.dispatch(coreActions.itemsLoaded(libraryItems));
}
run();
},
);
} else {
store.dispatch(uiActions.processFinished(action.type));
store.dispatch(coreActions.libraryLoaded({
uri: 'mopidy:library:tracks',
items_uris: arrayOf('uri', browseResponse),
}));
}
};
run();
});
break;
default:
return next(action);
next(action);
break;
}
};
}());

View File

@ -1,6 +1,7 @@
import ReactGA from 'react-ga';
import { uriType, generateGuid } from '../../util/helpers';
import { trackEvent } from '../../components/Trackable';
const coreActions = require('../core/actions');
const uiActions = require('../ui/actions');
@ -532,15 +533,14 @@ const PusherMiddleware = (function () {
/**
* Commands
**/
case 'PUSHER_GET_COMMANDS':
* */
case 'PUSHER_GET_COMMANDS': {
request(store, 'get_commands')
.then(
(response) => {
store.dispatch(pusherActions.commandsUpdated(response.commands));
},
(error) => {
() => {
// We're not too worried about capturing errors here
// It's also likely to fail where UI has been updated but
// server hasn't been restarted yet.
@ -549,24 +549,25 @@ const PusherMiddleware = (function () {
);
next(action);
break;
}
case 'PUSHER_SET_COMMAND':
var commands = { ...pusher.commands };
if (commands[action.command.id]) {
var command = { ...commands[action.command.id], ...action.command };
} else {
var { command } = action;
}
commands[action.command.id] = command;
case 'PUSHER_SET_COMMAND': {
const commands = {
...pusher.commands,
[action.command.id]: {
...(pusher.commands[action.command.id] || {}),
...action.command,
},
};
store.dispatch(pusherActions.setCommands(commands));
break;
}
case 'PUSHER_SET_COMMANDS':
case 'PUSHER_SET_COMMANDS': {
request(store, 'set_commands', { commands: action.commands })
.then(
(response) => {
() => {
// No action required, the change will be broadcast
},
(error) => {
@ -579,14 +580,15 @@ const PusherMiddleware = (function () {
next(action);
break;
}
case 'PUSHER_REMOVE_COMMAND':
var commands_index = { ...pusher.commands };
case 'PUSHER_REMOVE_COMMAND': {
const commands_index = { ...pusher.commands };
delete commands_index[action.id];
request(store, 'set_commands', { commands: commands_index })
.then(
(response) => {
() => {
// No action required, the change will be broadcast
},
(error) => {
@ -599,32 +601,34 @@ const PusherMiddleware = (function () {
next(action);
break;
}
case 'PUSHER_RUN_COMMAND':
var command = { ...pusher.commands[action.id] };
var notification_key = `command_${action.id}`;
case 'PUSHER_RUN_COMMAND': {
const notification_key = `command_${action.id}`;
if (action.notify) {
store.dispatch(uiActions.startProcess(notification_key, 'Running command'));
}
request(store, 'run_command', { id: action.id })
.then((response) => {
console.log('Command response', response);
.then(() => {
store.dispatch(uiActions.removeProcess(notification_key));
if (action.notify) {
store.dispatch(uiActions.createNotification({ key: notification_key, level: 'warning', content: 'Command sent' }));
store.dispatch(uiActions.createNotification({
key: notification_key,
content: 'Command sent',
}));
}
},
(error) => {
store.dispatch(uiActions.removeProcess(notification_key));
store.dispatch(coreActions.handleException(
'Could not run command',
error,
));
});
(error) => {
store.dispatch(uiActions.removeProcess(notification_key));
store.dispatch(coreActions.handleException(
'Could not run command',
error,
));
});
break;
}
/**
@ -654,26 +658,26 @@ const PusherMiddleware = (function () {
break;
case 'PUSHER_START_RADIO':
case 'PUSHER_UPDATE_RADIO':
if (store.getState().ui.allow_reporting) {
ReactGA.event({ category: 'Pusher', action: 'Start radio', label: action.uris.join() });
}
case 'PUSHER_UPDATE_RADIO': {
trackEvent({ category: 'Pusher', action: 'Start radio', label: action.uris.join() });
// start our UI process notification
if (action.type == 'PUSHER_UPDATE_RADIO') {
store.dispatch(uiActions.startProcess('PUSHER_RADIO_PROCESS', 'Updating radio'));
} else {
store.dispatch(uiActions.startProcess('PUSHER_RADIO_PROCESS', 'Starting radio'));
}
store.dispatch(
uiActions.startProcess(
action.type,
{
content: `${action.type === 'PUSHER_UPDATE_RADIO' ? 'Updating' : 'Starting'} radio`,
},
),
);
var data = {
reset: (action.type == 'PUSHER_START_RADIO'),
const data = {
reset: (action.type === 'PUSHER_START_RADIO'),
seed_artists: [],
seed_genres: [],
seed_tracks: [],
};
for (let i = 0; i < action.uris.length; i++) {
for (let i = 0; i < action.uris.length; i += 1) {
switch (uriType(action.uris[i])) {
case 'artist':
data.seed_artists.push(action.uris[i]);
@ -684,15 +688,16 @@ const PusherMiddleware = (function () {
case 'genre':
data.seed_genres.push(action.uris[i]);
break;
default:
break;
}
}
if (action.type == 'PUSHER_START_RADIO') {
if (action.type === 'PUSHER_START_RADIO') {
store.dispatch(pusherActions.deliverBroadcast(
'notification',
{
notification: {
level: 'warning',
content: `${pusher.username} is starting radio mode`,
},
},
@ -702,14 +707,14 @@ const PusherMiddleware = (function () {
request(store, 'change_radio', data)
.then(
(response) => {
store.dispatch(uiActions.processFinished('PUSHER_RADIO_PROCESS'));
if (response.status == 0) {
store.dispatch(uiActions.processFinished(action.type));
if (response.status === 0) {
store.dispatch(uiActions.createNotification({ content: response.message, level: 'error' }));
}
store.dispatch(pusherActions.radioChanged(response.radio));
},
(error) => {
store.dispatch(uiActions.processFinished('PUSHER_RADIO_PROCESS'));
store.dispatch(uiActions.processFinished(action.type));
store.dispatch(coreActions.handleException(
'Could not change radio',
error,
@ -717,25 +722,22 @@ const PusherMiddleware = (function () {
},
);
break;
}
case 'PUSHER_STOP_RADIO':
case 'PUSHER_STOP_RADIO': {
store.dispatch(uiActions.createNotification({ content: 'Stopping radio' }));
if (store.getState().ui.allow_reporting) {
ReactGA.event({ category: 'Pusher', action: 'Stop radio' });
}
trackEvent({ category: 'Pusher', action: 'Stop radio' });
store.dispatch(pusherActions.deliverBroadcast(
'notification',
{
notification: {
level: 'warning',
content: `${pusher.username} stopped radio mode`,
},
},
));
var data = {
const data = {
seed_artists: [],
seed_genres: [],
seed_tracks: [],
@ -753,29 +755,18 @@ const PusherMiddleware = (function () {
},
);
break;
/**
* Notifications and alerts
* */
}
case 'PUSHER_BROWSER_NOTIFICATION':
store.dispatch(uiActions.createBrowserNotification(action));
break;
case 'PUSHER_NOTIFICATION':
var data = {
store.dispatch(uiActions.createNotification({
...action, type: action.notification_type,
};
store.dispatch(uiActions.createNotification(data));
}));
break;
/**
* Server actions
* */
case 'PUSHER_RELOAD':
// Hard reload. This doesn't strictly clear the cache, but our compiler's
// cache buster should handle that
@ -812,12 +803,11 @@ const PusherMiddleware = (function () {
next(action);
break;
case 'PUSHER_CONFIG':
case 'PUSHER_CONFIG': {
// Set default country/locale (unless we've already been configured)
var { spotify } = store.getState();
var spotify_updated = false;
var spotify_updates = {};
const { spotify } = store.getState();
const spotify_updates = {};
let spotify_updated = false;
if (!spotify.country && action.config.country) {
spotify_updates.country = action.config.country;
@ -848,6 +838,7 @@ const PusherMiddleware = (function () {
next(action);
break;
}
case 'PUSHER_DEBUG':
request(store, action.message.method, action.message.data)
@ -867,13 +858,9 @@ const PusherMiddleware = (function () {
case 'PUSHER_ERROR':
store.dispatch(uiActions.createNotification(action.message, 'bad'));
if (store.getState().ui.allow_reporting) {
ReactGA.event({ category: 'Pusher', action: 'Error', label: action.message });
}
trackEvent({ category: 'Pusher', action: 'Error', label: action.message });
break;
// This action is irrelevant to us, pass it on to the next middleware
default:
return next(action);
}

View File

@ -342,7 +342,7 @@ export function getTrack(uri) {
);
};
}
/*
export function getLibraryTracks() {
return (dispatch, getState) => {
request(dispatch, getState, 'me/tracks?limit=50')
@ -362,6 +362,7 @@ export function getLibraryTracks() {
);
};
}
*/
export function getFeaturedPlaylists(forceRefetch = false) {
return (dispatch, getState) => {
@ -564,8 +565,9 @@ export function getMore(url, core_action = null, custom_action = null, extra_dat
}
export function getSearchResults({ type, term }, limit = 50, offset = 0) {
const processKey = 'SPOTIFY_GET_SEARCH_RESULTS';
return (dispatch, getState) => {
dispatch(uiActions.startProcess('SPOTIFY_GET_SEARCH_RESULTS_PROCESSOR', 'Searching Spotify'));
dispatch(uiActions.startProcess(processKey, 'Searching Spotify'));
let typeString = type.replace(/s+$/, '');
if (typeString === 'all') {
@ -617,7 +619,7 @@ export function getSearchResults({ type, term }, limit = 50, offset = 0) {
));
}
dispatch(uiActions.processFinished('SPOTIFY_GET_SEARCH_RESULTS_PROCESSOR'));
dispatch(uiActions.processFinished(processKey));
},
(error) => {
dispatch(coreActions.handleException(
@ -1320,7 +1322,7 @@ export function savePlaylist(uri, name, description, is_public, is_collaborative
};
}
export function getPlaylist(uri, { full, forceRefetch, callbackAction }) {
export function getPlaylist(uri, { full, forceRefetch, callbackAction } = {}) {
return (dispatch, getState) => {
let endpoint = `playlists/${getFromUri('playlistid', uri)}`;
endpoint += `?market=${getState().spotify.country}`;
@ -1566,24 +1568,18 @@ export function reorderPlaylistTracks(uri, range_start, range_length, insert_bef
* ======================================================================================
* */
export function flushLibrary() {
return {
type: 'SPOTIFY_FLUSH_LIBRARY',
};
}
export function getLibraryPlaylists(forceRefetch) {
return (dispatch, getState) => {
const processKey = 'SPOTIFY_GET_LIBRARY_PLAYLISTS';
dispatch(uiActions.updateProcess(processKey, { notification: false }));
dispatch(uiActions.startProcess(processKey, { notification: false }));
let libraryItems = [];
const fetchLibraryPlaylists = (endpoint) => request(dispatch, getState, endpoint)
const fetch = (endpoint) => request(dispatch, getState, endpoint)
.then((response) => {
const processor = getState().ui.processes[processKey];
if (processor && processor.status === 'cancelling') {
dispatch(uiActions.stopLoading('spotify:library:playlists'));
dispatch(uiActions.processCancelled(processKey));
dispatch(uiActions.stopLoading('spotify:library:albums'));
return;
}
@ -1601,7 +1597,7 @@ export function getLibraryPlaylists(forceRefetch) {
);
libraryItems = [...libraryItems, ...items];
if (response.next) {
fetchLibraryPlaylists(`${response.next}${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
fetch(`${response.next}${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
} else {
dispatch(uiActions.processFinished(processKey));
dispatch(coreActions.itemsLoaded(libraryItems));
@ -1612,17 +1608,17 @@ export function getLibraryPlaylists(forceRefetch) {
}
});
fetchLibraryPlaylists(`me/playlists?limit=50${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
fetch(`me/playlists?limit=50${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
};
}
export function getLibraryAlbums(forceRefetch) {
return (dispatch, getState) => {
const processKey = 'SPOTIFY_GET_LIBRARY_ALBUMS';
dispatch(uiActions.updateProcess(processKey, { notification: false }));
dispatch(uiActions.startProcess(processKey, { notification: false }));
let libraryItems = [];
const fetchLibraryAlbums = (endpoint) => request(dispatch, getState, endpoint)
const fetch = (endpoint) => request(dispatch, getState, endpoint)
.then((response) => {
const processor = getState().ui.processes[processKey];
if (processor && processor.status === 'cancelling') {
@ -1645,7 +1641,7 @@ export function getLibraryAlbums(forceRefetch) {
libraryItems = [...libraryItems, ...items];
if (response.next) {
fetchLibraryAlbums(`${response.next}${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
fetch(`${response.next}${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
} else {
dispatch(uiActions.processFinished(processKey));
dispatch(coreActions.itemsLoaded(libraryItems));
@ -1656,17 +1652,17 @@ export function getLibraryAlbums(forceRefetch) {
}
});
fetchLibraryAlbums(`me/albums?limit=50${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
fetch(`me/albums?limit=50${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
};
}
export function getLibraryArtists(forceRefetch) {
return (dispatch, getState) => {
const processKey = 'SPOTIFY_GET_LIBRARY_ARTISTS';
dispatch(uiActions.updateProcess(processKey, { notification: false }));
dispatch(uiActions.startProcess(processKey, { notification: false }));
let libraryItems = [];
const fetchLibraryArtists = (endpoint) => request(dispatch, getState, endpoint)
const fetch = (endpoint) => request(dispatch, getState, endpoint)
.then((response) => {
const processor = getState().ui.processes[processKey];
if (processor && processor.status === 'cancelling') {
@ -1677,7 +1673,10 @@ export function getLibraryArtists(forceRefetch) {
dispatch(uiActions.updateProcess(
processKey,
{ total: response.artists.total, remaining: response.artists.total - libraryItems.length },
{
total: response.artists.total,
remaining: response.artists.total - libraryItems.length,
},
));
const items = response.artists.items.map(
@ -1688,7 +1687,7 @@ export function getLibraryArtists(forceRefetch) {
);
libraryItems = [...libraryItems, ...items];
if (response.next) {
fetchLibraryArtists(`${response.next}${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
fetch(`${response.next}${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
} else {
dispatch(uiActions.processFinished(processKey));
dispatch(coreActions.itemsLoaded(libraryItems));
@ -1699,6 +1698,49 @@ export function getLibraryArtists(forceRefetch) {
}
});
fetchLibraryArtists(`me/following?type=artist&limit=50${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
fetch(`me/following?type=artist&limit=50${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
};
}
export function getLibraryTracks(forceRefetch) {
return (dispatch, getState) => {
const processKey = 'SPOTIFY_GET_LIBRARY_TRACKS';
dispatch(uiActions.startProcess(processKey, { notification: false }));
let libraryItems = [];
const fetch = (endpoint) => request(dispatch, getState, endpoint)
.then((response) => {
const processor = getState().ui.processes[processKey];
if (processor && processor.status === 'cancelling') {
dispatch(uiActions.processCancelled(processKey));
dispatch(uiActions.stopLoading('spotify:library:tracks'));
return;
}
dispatch(uiActions.updateProcess(
processKey,
{ total: response.total, remaining: response.total - libraryItems.length },
));
const items = response.items.map(
(item) => ({
...formatTrack(item),
in_library: true,
}),
);
libraryItems = [...libraryItems, ...items];
if (response.next) {
fetch(`${response.next}${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
} else {
dispatch(uiActions.processFinished(processKey));
dispatch(coreActions.itemsLoaded(libraryItems));
dispatch(coreActions.libraryLoaded({
uri: 'spotify:library:tracks',
items_uris: arrayOf('uri', libraryItems),
}));
}
});
fetch(`me/tracks?limit=50${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
};
}

View File

@ -298,10 +298,10 @@ export function runProcess(key, data = {}) {
};
}
export function cancelProcess(key) {
export function cancelProcess(keys) {
return {
type: 'CANCEL_PROCESS',
key,
keys: Array.isArray(keys) ? keys : [keys],
};
}

View File

@ -148,9 +148,16 @@ export default function reducer(ui = {}, action) {
case 'CANCEL_PROCESS': {
const processes = { ...(ui.processes ? ui.processes : {}) };
if (processes[action.key]) {
processes[action.key] = { ...processes[action.key], status: 'cancelling' };
}
action.keys.forEach((key) => {
if (processes[key]) {
processes[key] = {
...processes[key],
status: 'cancelling',
total: undefined,
remaining: undefined,
};
}
});
return { ...ui, processes };
}

View File

@ -56,15 +56,17 @@ const makeSearchResultsSelector = (term, type) => createSelector(
const makeProcessProgressSelector = (keys) => createSelector(
[getProcesses],
(processes) => {
const selectedProcesses = keys.map((key) => processes[key] || {});
const selectedProcesses = keys
.map((key) => processes[key] || {})
.filter((i) => i.status === 'running');
let total = 0;
let remaining = 0;
selectedProcesses.forEach((process) => {
if (process.total) total += process.total;
if (process.remaining) remaining += process.remaining;
});
console.log({ total, remaining })
return total && remaining ? ((total - remaining) / total).toFixed(4) : 0;
return total && remaining ? ((total - remaining) / total).toFixed(4) : 1;
},
);

View File

@ -20,10 +20,15 @@ import { i18n, I18n } from '../../locale';
import Loader from '../../components/Loader';
import {
makeLibrarySelector,
makeLoadingSelector,
makeProcessProgressSelector,
} from '../../util/selectors';
const processKeys = [
'MOPIDY_GET_LIBRARY_ALBUMS',
'SPOTIFY_GET_LIBRARY_ALBUMS',
'GOOGLE_GET_LIBRARY_ALBUMS',
];
class LibraryAlbums extends React.Component {
constructor(props) {
super(props);
@ -68,7 +73,7 @@ class LibraryAlbums extends React.Component {
}
}
onRefresh = () => {
refresh = () => {
const { uiActions: { hideContextMenu } } = this.props;
hideContextMenu();
@ -77,6 +82,13 @@ class LibraryAlbums extends React.Component {
this.getSpotifyLibrary(true);
}
cancelRefresh = () => {
const { uiActions: { hideContextMenu, cancelProcess } } = this.props;
hideContextMenu();
cancelProcess(processKeys);
}
getMopidyLibrary = (forceRefetch = false) => {
const {
source,
@ -175,7 +187,6 @@ class LibraryAlbums extends React.Component {
sort,
sort_reverse,
view,
loading,
loading_progress,
} = this.props;
const {
@ -184,9 +195,8 @@ class LibraryAlbums extends React.Component {
} = this.state;
let { albums } = this.props;
if (loading) {
console.log(loading_progress);
return <Loader body loading progress={loading_progress} />
if (loading_progress < 1) {
return <Loader body loading progress={loading_progress} />;
}
if (sort) {
@ -245,11 +255,13 @@ class LibraryAlbums extends React.Component {
source,
sort_reverse,
uiActions,
loading_progress,
} = this.props;
const {
filter,
per_page,
} = this.state;
const loading = loading_progress < 1;
const source_options = [
{
@ -349,11 +361,11 @@ class LibraryAlbums extends React.Component {
<Button
noHover
discrete
onClick={this.onRefresh}
onClick={loading ? this.cancelRefresh : this.refresh}
tracking={{ category: 'LibraryAlbums', action: 'Refresh' }}
>
<Icon name="refresh" />
<I18n path="actions.refresh" />
{loading ? <Icon name="close" /> : <Icon name="refresh" /> }
{loading ? <I18n path="actions.cancel" /> : <I18n path="actions.refresh" /> }
</Button>
</div>
);
@ -372,21 +384,15 @@ class LibraryAlbums extends React.Component {
const mapStateToProps = (state) => {
const source = state.ui.library_albums_source ? state.ui.library_albums_source : 'all';
const loadingSelector = makeLoadingSelector(['(.*):library:albums']);
const libraryUris = [];
if (source === 'all' || source === 'local') libraryUris.push('mopidy:library:albums');
if (source === 'all' || source === 'spotify') libraryUris.push('spotify:library:albums');
if (source === 'all' || source === 'google') libraryUris.push('google:library:albums');
const librarySelector = makeLibrarySelector(libraryUris);
const processProgressSelector = makeProcessProgressSelector([
'MOPIDY_GET_LIBRARY_ALBUMS',
'SPOTIFY_GET_LIBRARY_ALBUMS',
'GOOGLE_GET_LIBRARY_ALBUMS',
]);
const processProgressSelector = makeProcessProgressSelector(processKeys);
return {
loading: loadingSelector(state),
loading_progress: processProgressSelector(state),
mopidy_uri_schemes: state.mopidy.uri_schemes,
albums: librarySelector(state),

View File

@ -21,6 +21,12 @@ import {
makeProcessProgressSelector,
} from '../../util/selectors';
const processKeys = [
'MOPIDY_GET_LIBRARY_ARTISTS',
'SPOTIFY_GET_LIBRARY_ARTISTS',
'GOOGLE_GET_LIBRARY_ARTISTS',
];
class LibraryArtists extends React.Component {
constructor(props) {
super(props);
@ -63,7 +69,7 @@ class LibraryArtists extends React.Component {
}
}
onRefresh = () => {
refresh = () => {
const { uiActions: { hideContextMenu } } = this.props;
hideContextMenu();
@ -72,6 +78,13 @@ class LibraryArtists extends React.Component {
this.getSpotifyLibrary(true);
}
cancelRefresh = () => {
const { uiActions: { hideContextMenu, cancelProcess } } = this.props;
hideContextMenu();
cancelProcess(processKeys);
}
setSort = (value) => {
const { sort, sort_reverse, uiActions: { set } } = this.props;
let reverse = false;
@ -151,7 +164,6 @@ class LibraryArtists extends React.Component {
sort,
sort_reverse,
view,
loading,
loading_progress,
} = this.props;
const {
@ -160,7 +172,7 @@ class LibraryArtists extends React.Component {
} = this.state;
let { artists } = this.props;
if (loading) {
if (loading_progress < 1) {
return <Loader body loading progress={loading_progress} />
}
@ -215,7 +227,9 @@ class LibraryArtists extends React.Component {
const {
spotify_available,
google_available,
loading_progress,
} = this.props;
const loading = loading_progress < 1;
const source_options = [
{
@ -307,11 +321,11 @@ class LibraryArtists extends React.Component {
<Button
noHover
discrete
onClick={this.onRefresh}
onClick={loading ? this.cancelRefresh : this.refresh}
tracking={{ category: 'LibraryArtists', action: 'Refresh' }}
>
<Icon name="refresh" />
<I18n path="actions.refresh" />
{loading ? <Icon name="close" /> : <Icon name="refresh" /> }
{loading ? <I18n path="actions.cancel" /> : <I18n path="actions.refresh" /> }
</Button>
</span>
);
@ -330,18 +344,13 @@ class LibraryArtists extends React.Component {
const mapStateToProps = (state) => {
const source = state.ui.library_artists_source || 'all';
const loadingSelector = makeLoadingSelector(['(.*):library:artists']);
const libraryUris = [];
if (source === 'all' || source === 'local') libraryUris.push('mopidy:library:artists');
if (source === 'all' || source === 'spotify') libraryUris.push('spotify:library:artists');
if (source === 'all' || source === 'google') libraryUris.push('google:library:artists');
const librarySelector = makeLibrarySelector(libraryUris);
const processProgressSelector = makeProcessProgressSelector([
'MOPIDY_GET_LIBRARY_ARTISTS',
'SPOTIFY_GET_LIBRARY_ARTISTS',
'GOOGLE_GET_LIBRARY_ARTISTS',
]);
const processProgressSelector = makeProcessProgressSelector(processKeys);
return {
loading_progress: processProgressSelector(state),
@ -349,7 +358,6 @@ const mapStateToProps = (state) => {
google_available: (state.mopidy.uri_schemes && state.mopidy.uri_schemes.includes('gmusic:')),
spotify_available: (state.spotify.access_token),
artists: librarySelector(state),
loading: loadingSelector(state),
source,
sort: (state.ui.library_artists_sort ? state.ui.library_artists_sort : null),
sort_reverse: (state.ui.library_artists_sort_reverse ? state.ui.library_artists_sort_reverse : false),

View File

@ -16,10 +16,17 @@ import * as mopidyActions from '../../services/mopidy/actions';
import * as spotifyActions from '../../services/spotify/actions';
import { applyFilter, removeDuplicates, sortItems } from '../../util/arrays';
import { I18n, i18n } from '../../locale';
import { collate } from '../../util/format';
import { isLoading } from '../../util/helpers';
import Loader from '../../components/Loader';
import { makeLibrarySelector, makeLoadingSelector } from '../../util/selectors';
import {
makeLibrarySelector,
makeLoadingSelector,
makeProcessProgressSelector,
} from '../../util/selectors';
const processKeys = [
'MOPIDY_GET_LIBRARY_PLAYLISTS',
'SPOTIFY_GET_LIBRARY_PLAYLISTS',
];
class LibraryPlaylists extends React.Component {
constructor(props) {
@ -55,7 +62,7 @@ class LibraryPlaylists extends React.Component {
this.getSpotifyLibrary();
}
onRefresh = () => {
refresh = () => {
const { uiActions: { hideContextMenu } } = this.props;
hideContextMenu();
@ -63,6 +70,13 @@ class LibraryPlaylists extends React.Component {
this.getSpotifyLibrary(true);
}
cancelRefresh = () => {
const { uiActions: { hideContextMenu, cancelProcess } } = this.props;
hideContextMenu();
cancelProcess(processKeys);
}
getMopidyLibrary = (forceRefetch = false) => {
const {
source,
@ -137,7 +151,7 @@ class LibraryPlaylists extends React.Component {
sort,
sort_reverse,
view,
loading,
loading_progress,
playlists: playlistsProp,
} = this.props;
const {
@ -145,8 +159,8 @@ class LibraryPlaylists extends React.Component {
limit,
} = this.state;
if (loading) {
return <Loader body loading />;
if (loading_progress < 1) {
return <Loader body loading progress={loading_progress} />;
}
let playlists = [...playlistsProp];
@ -206,10 +220,12 @@ class LibraryPlaylists extends React.Component {
sort,
sort_reverse,
view,
loading_progress,
} = this.props;
const {
filter,
} = this.state;
const loading = loading_progress < 1;
const source_options = [
{
@ -306,11 +322,11 @@ class LibraryPlaylists extends React.Component {
<Button
noHover
discrete
onClick={this.onRefresh}
onClick={loading ? this.cancelRefresh : this.refresh}
tracking={{ category: 'LibraryAlbums', action: 'Refresh' }}
>
<Icon name="refresh" />
<I18n path="actions.refresh" />
{loading ? <Icon name="close" /> : <Icon name="refresh" /> }
{loading ? <I18n path="actions.cancel" /> : <I18n path="actions.refresh" /> }
</Button>
<Button
to="/playlist/create"
@ -338,20 +354,20 @@ class LibraryPlaylists extends React.Component {
const mapStateToProps = (state) => {
const source = state.ui.library_playlists_source || 'all';
const loadingSelector = makeLoadingSelector(['(.*):library:playlists']);
const libraryUris = [];
if (source === 'all' || source === 'local') libraryUris.push('mopidy:library:playlists');
if (source === 'all' || source === 'spotify') libraryUris.push('spotify:library:playlists');
if (source === 'all' || source === 'google') libraryUris.push('google:library:playlists');
const librarySelector = makeLibrarySelector(libraryUris);
const processProgressSelector = makeProcessProgressSelector(processKeys);
return {
slim_mode: state.ui.slim_mode,
mopidy_uri_schemes: state.mopidy.uri_schemes,
spotify_available: state.spotify.access_token,
playlists: librarySelector(state),
loading: loadingSelector(state),
loading_progress: processProgressSelector(state),
source,
me_id: (state.spotify.me ? state.spotify.me.id : false),
view: state.ui.library_playlists_view,

View File

@ -2,116 +2,336 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Loader from '../../components/Loader';
import TrackList from '../../components/TrackList';
import Header from '../../components/Header';
import DropdownField from '../../components/Fields/DropdownField';
import FilterField from '../../components/Fields/FilterField';
import LazyLoadListener from '../../components/LazyLoadListener';
import Icon from '../../components/Icon';
import * as coreActions from '../../services/core/actions';
import * as uiActions from '../../services/ui/actions';
import * as mopidyActions from '../../services/mopidy/actions';
import * as spotifyActions from '../../services/spotify/actions';
import { isLoading } from '../../util/helpers';
import { I18n, i18n } from '../../locale';
import { sortItems, applyFilter } from '../../util/arrays';
import Button from '../../components/Button';
import { i18n, I18n } from '../../locale';
import Loader from '../../components/Loader';
import {
makeLibrarySelector,
makeProcessProgressSelector,
} from '../../util/selectors';
const processKeys = [
'MOPIDY_GET_LIBRARY_TRACKS',
'SPOTIFY_GET_LIBRARY_TRACKS',
];
class LibraryTracks extends React.Component {
componentDidMount() {
this.props.uiActions.setWindowTitle(i18n('library.tracks.title'));
constructor(props) {
super(props);
if (!this.props.spotify_available) {
this.props.uiActions.createNotification({
level: 'warning',
content: i18n('errors.enable_first', { provider: i18n('services.spotify.title') }),
this.state = {
filter: '',
limit: 50,
per_page: 50,
};
}
componentDidMount() {
const {
location: {
state = {},
},
uiActions: {
setWindowTitle,
},
} = this.props;
// Restore any limit defined in our location state
if (state.limit) {
this.setState({
limit: state.limit,
});
} else if (this.props.library_tracks === undefined) {
this.props.spotifyActions.getLibraryTracks();
}
setWindowTitle(i18n('library.tracks.title'));
this.getMopidyLibrary();
this.getSpotifyLibrary();
}
componentDidUpdate = ({ source: prevSource }) => {
const { source } = this.props;
if (source !== prevSource) {
this.getMopidyLibrary();
this.getSpotifyLibrary();
}
}
loadMore() {
this.props.spotifyActions.getMore(
this.props.library_tracks_more,
null,
{
type: 'SPOTIFY_LIBRARY_TRACKS_LOADED_MORE',
refresh = () => {
const { uiActions: { hideContextMenu } } = this.props;
hideContextMenu();
this.getMopidyLibrary(true);
this.getSpotifyLibrary(true);
}
cancelRefresh = () => {
const { uiActions: { hideContextMenu, cancelProcess } } = this.props;
hideContextMenu();
cancelProcess(processKeys);
}
getMopidyLibrary = (forceRefetch = false) => {
const {
source,
coreActions: {
loadLibrary,
},
} = this.props;
if (source !== 'local' && source !== 'all') return;
loadLibrary('mopidy:library:tracks', { forceRefetch });
};
getSpotifyLibrary = (forceRefetch = false) => {
const {
source,
spotify_available,
coreActions: {
loadLibrary,
},
} = this.props;
if (!spotify_available) return;
if (source !== 'spotify' && source !== 'all') return;
loadLibrary('spotify:library:tracks', { forceRefetch });
};
handleContextMenu = (e, item) => {
const {
uiActions: {
showContextMenu,
},
} = this.props;
showContextMenu({
e,
context: 'album',
uris: [item.uri],
items: [item],
});
}
loadMore = () => {
const {
limit,
per_page,
} = this.state;
const {
location: {
state,
},
history,
} = this.props;
const new_limit = limit + per_page;
this.setState({ limit: new_limit });
history.replace({ state: { ...state, limit: new_limit } });
}
setSort = (value) => {
const {
sort,
sort_reverse,
uiActions: {
set,
},
} = this.props;
let reverse = false;
if (sort === value) reverse = !sort_reverse;
set({
library_tracks_sort_reverse: reverse,
library_tracks_sort: value,
});
}
renderView = () => {
const {
sort,
sort_reverse,
loading_progress,
} = this.props;
const {
limit,
filter,
} = this.state;
let { tracks } = this.props;
if (loading_progress < 1) {
return <Loader body loading progress={loading_progress} />;
}
if (sort) {
tracks = sortItems(tracks, sort, sort_reverse);
}
if (filter && filter !== '') {
tracks = applyFilter('name', filter, tracks);
}
// Apply our lazy-load-rendering
const total_tracks = tracks.length;
tracks = tracks.slice(0, limit);
return (
<section className="content-wrapper">
<TrackList tracks={tracks} />
<LazyLoadListener
loadKey={total_tracks > limit ? limit : total_tracks}
showLoader={limit < total_tracks}
loadMore={this.loadMore}
/>
</section>
);
}
playAll() {
this.props.spotifyActions.getLibraryTracksAndPlay();
}
render = () => {
const {
spotify_available,
sort,
source,
sort_reverse,
uiActions,
loading_progress,
} = this.props;
const {
filter,
per_page,
} = this.state;
const loading = loading_progress < 1;
render() {
// Note trailing "?" makes sure our context menu in_library checks doesn't interfere
if (isLoading(this.props.load_queue, ['spotify_me/tracks?'])) {
return (
<div className="view library-tracks-view">
<Header icon="music" title={i18n('library.tracks.title')} />
<Loader body loading />
</div>
);
const source_options = [
{
value: 'all',
label: i18n('fields.filters.all'),
},
{
value: 'local',
label: i18n('services.mopidy.local'),
},
];
if (spotify_available) {
source_options.push({
value: 'spotify',
label: i18n('services.spotify.title'),
});
}
const tracks = [];
if (this.props.library_tracks && this.props.tracks) {
for (let i = 0; i < this.props.library_tracks.length; i++) {
const uri = this.props.library_tracks[i];
if (this.props.tracks.hasOwnProperty(uri)) {
tracks.push(this.props.tracks[uri]);
}
}
}
const sort_options = [
{
value: null,
label: i18n('fields.filters.as_loaded'),
},
{
value: 'name',
label: i18n('fields.filters.name'),
},
{
value: 'artists.first.name',
label: i18n('fields.filters.artist'),
},
{
value: 'last_modified',
label: i18n('fields.filters.updated'),
},
{
value: 'tracks_uris.length',
label: i18n('fields.filters.tracks'),
},
{
value: 'uri',
label: i18n('fields.filters.source'),
},
];
const options = (
<div className="header__options__wrapper">
<FilterField
initialValue={filter}
handleChange={(value) => this.setState({ filter: value, limit: per_page })}
onSubmit={() => uiActions.hideContextMenu()}
/>
<DropdownField
icon="swap_vert"
name={i18n('fields.sort')}
value={sort}
valueAsLabel
options={sort_options}
selected_icon={sort ? (sort_reverse ? 'keyboard_arrow_up' : 'keyboard_arrow_down') : null}
handleChange={(val) => { this.setSort(val); uiActions.hideContextMenu(); }}
/>
<DropdownField
icon="cloud"
name={i18n('fields.source')}
value={source}
valueAsLabel
options={source_options}
handleChange={(val) => { uiActions.set({ library_tracks_source: val }); uiActions.hideContextMenu(); }}
/>
<Button
onClick={(e) => this.playAll(e)}
tracking={{ category: 'LibraryTracks', action: 'Play' }}
noHover
discrete
onClick={loading ? this.cancelRefresh : this.refresh}
tracking={{ category: 'LibraryTracks', action: 'Refresh' }}
>
<Icon name="play_circle_filled" />
<I18n path="actions.play_all" />
{loading ? <Icon name="close" /> : <Icon name="refresh" /> }
{loading ? <I18n path="actions.cancel" /> : <I18n path="actions.refresh" /> }
</Button>
</div>
);
return (
<div className="view library-tracks-view">
<Header options={options} uiActions={this.props.uiActions}>
<Icon name="music_note" type="material" />
<I18n path="library.tracks.title" />
<Header options={options} uiActions={uiActions}>
<Icon name="album" type="material" />
<I18n path="library.tracks.title" />
</Header>
<section className="content-wrapper">
<TrackList tracks={tracks} />
<LazyLoadListener
loadKey={this.props.library_tracks_more}
showLoader={this.props.library_tracks_more}
loadMore={() => this.loadMore()}
/>
</section>
{this.renderView()}
</div>
);
}
}
const mapStateToProps = (state) => {
const source = state.ui.library_tracks_source ? state.ui.library_tracks_source : 'all';
/**
* Export our component
*
* We also integrate our global store, using connect()
* */
const libraryUris = [];
if (source === 'all' || source === 'local') libraryUris.push('mopidy:library:tracks');
if (source === 'all' || source === 'spotify') libraryUris.push('spotify:library:tracks');
const librarySelector = makeLibrarySelector(libraryUris);
const processProgressSelector = makeProcessProgressSelector(processKeys);
const mapStateToProps = (state, ownProps) => ({
load_queue: state.ui.load_queue,
tracks: state.core.tracks,
spotify_available: state.spotify.access_token,
library_tracks: state.spotify.library_tracks,
library_tracks_more: state.spotify.library_tracks_more,
});
return {
loading_progress: processProgressSelector(state),
mopidy_uri_schemes: state.mopidy.uri_schemes,
tracks: librarySelector(state),
spotify_available: state.spotify.access_token,
view: state.ui.library_tracks_view,
source,
sort: state.ui.library_tracks_sort,
sort_reverse: state.ui.library_tracks_sort_reverse,
};
};
const mapDispatchToProps = (dispatch) => ({
coreActions: bindActionCreators(coreActions, dispatch),
uiActions: bindActionCreators(uiActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch),
spotifyActions: bindActionCreators(spotifyActions, dispatch),