Debugging sorting issue #379; Fixing keyboard shortcuts #384

This commit is contained in:
James Barnsley
2019-04-08 07:17:27 +12:00
parent 0a6dd7bf23
commit 4e93234d12
13 changed files with 152 additions and 98 deletions

View File

@ -1708,9 +1708,13 @@ var sortItems = exports.sortItems = function sortItems(array, property) {
// Apply sort on a property of the first item of an array
if (property_element == 'first' && Array.isArray(value) && value.length > 0) {
value = value[0];
continue;
if (property_element == 'first') {
if (Array.isArray(value) && value.length > 0) {
value = value[0];
continue;
} else {
return null;
}
// Just need the length of an array
} else if (property_element == 'length') {
@ -1774,7 +1778,9 @@ var sortItems = exports.sortItems = function sortItems(array, property) {
// Alphabetic sorting
} else if (typeof a_value === 'string') {
if (!a_value || !b_value) return 0;
if (a_value && !b_value) return -1;
if (!a_value && b_value) return 1;
if (!a_value && !b_value) return 0;
if (a_value.toLowerCase() > b_value.toLowerCase()) return 1;
if (a_value.toLowerCase() < b_value.toLowerCase()) return -1;
return 0;
@ -3088,6 +3094,10 @@ var Icon = function (_React$Component) {
value: function render() {
var _this2 = this;
if (!this.props.name || this.props.name === "") {
return null;
}
var className = "icon icon--" + (this.props.type ? this.props.type : 'material');
if (this.props.className) {
className += ' ' + this.props.className;
@ -7848,9 +7858,8 @@ function getImages(context, uri) {
if (params) {
sendRequest(dispatch, getState, params).then(function (response) {
if (response.album) {
record = Object.assign({}, record, { images: response.album.image });
dispatch(coreActions.trackLoaded(record));
dispatch(coreActions.albumLoaded(response.album));
var images = helpers.formatImages(response.album.image);
dispatch(coreActions.trackLoaded({ uri: uri, images: images }));
}
});
}
@ -57344,7 +57353,7 @@ var PusherMiddleware = function () {
break;
case 'local_scan_error':
store.dispatch(uiActions.processFinished('local_scan'));
store.dispatch(uiActions.createNotification({ key: 'local_scan', type: 'bad', content: 'Local scan failed' }));
store.dispatch(uiActions.createNotification({ type: 'bad', content: message.params.message, description: message.params.description }));
break;
// Upgrade
@ -57356,7 +57365,7 @@ var PusherMiddleware = function () {
break;
case 'upgrade_error':
store.dispatch(uiActions.processFinished('upgrade'));
store.dispatch(uiActions.createNotification({ type: 'bad', content: 'Upgrade failed' }));
store.dispatch(uiActions.createNotification({ type: 'bad', content: message.params.message, description: message.params.description }));
break;
// Restart
@ -57364,6 +57373,10 @@ var PusherMiddleware = function () {
store.dispatch(uiActions.processFinished('upgrade'));
store.dispatch(uiActions.createNotification({ type: 'info', content: 'Restarting server...' }));
break;
case 'restart_error':
store.dispatch(uiActions.processFinished('upgrade'));
store.dispatch(uiActions.createNotification({ type: 'bad', content: message.params.message, description: message.params.description }));
break;
// Test
case 'test_started':
@ -57375,7 +57388,7 @@ var PusherMiddleware = function () {
break;
case 'test_error':
store.dispatch(uiActions.processFinished('test'));
store.dispatch(uiActions.createNotification({ type: 'bad', content: 'Test failed' }));
store.dispatch(uiActions.createNotification({ type: 'bad', content: message.params.message, description: message.params.description }));
break;
}
@ -64731,10 +64744,10 @@ var App = exports.App = function (_React$Component) {
}, {
key: 'shouldTriggerShortcut',
value: function shouldTriggerShortcut(e) {
if (!this.props.shortkeys_enabled) {
return false;
}
var keyCode = e.keyCode || e.which;
// When we're focussed on certian elements, don't fire any shortcuts
// Typically form inputs
@ -64743,23 +64756,20 @@ var App = exports.App = function (_React$Component) {
return false;
}
// Listen for standalone key codes
var keyCodes = [27, 32, 191];
if (keyCodes.indexOf(e.keyCode) > -1) {
var hotkeys = [27, 32, 191];
if (hotkeys.indexOf(keyCode) > -1) {
e.preventDefault();
return true;
}
// Listen for key codes that require ctrl to be held
var keyCodesWithCtrl = [37, 38, 39, 40];
if ((e.ctrlKey || e.metaKey) && keyCodesWithCtrl.indexOf(e.keyCode) > -1) {
var hokeys_with_alt = [37, 38, 39, 40];
if (e.altKey && hokeys_with_alt.indexOf(keyCode) > -1) {
e.preventDefault();
return true;
}
// Listen for key codes that require ctrl to be held
var keyCodesWithCtrlShift = [70];
if ((e.ctrlKey || e.metaKey) && e.shiftKey && keyCodesWithCtrlShift.indexOf(e.keyCode) > -1) {
var hokeys_with_alt_and_shift = [70];
if (e.altKey && e.shiftKey && hokeys_with_alt_and_shift.indexOf(keyCode) > -1) {
e.preventDefault();
return true;
}
@ -64815,7 +64825,7 @@ var App = exports.App = function (_React$Component) {
return;
}
switch (e.keyCode) {
switch (e.keyCode || e.which) {
case 32:
// spacebar
@ -64840,10 +64850,10 @@ var App = exports.App = function (_React$Component) {
case 40:
// down
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
if (e.altKey && e.shiftKey) {
this.props.mopidyActions.setMute(true);
this.props.uiActions.createNotification({ content: 'volume-off', type: 'shortcut', key: 'shortcut', duration: 1 });
} else if (e.ctrlKey) {
} else if (e.altKey) {
var volume = this.props.volume;
if (volume !== 'false') {
volume -= 5;
@ -64861,13 +64871,13 @@ var App = exports.App = function (_React$Component) {
case 38:
// up
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
if (e.altKey && e.shiftKey) {
this.props.mopidyActions.setVolume(100);
if (this.props.mute) {
this.props.mopidyActions.setMute(false);
}
this.props.uiActions.createNotification({ content: 'volume-up', type: 'shortcut', key: 'shortcut', duration: 1 });
} else if (e.ctrlKey || e.metaKey) {
} else if (e.altKey) {
var volume = this.props.volume;
if (volume !== 'false') {
volume += 5;
@ -64885,14 +64895,14 @@ var App = exports.App = function (_React$Component) {
case 37:
// left
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
if (e.altKey && e.shiftKey) {
var new_position = this.props.play_time_position - 30000;
if (new_position < 0) {
new_position = 0;;
}
this.props.mopidyActions.seek(new_position);
this.props.mopidyActions.setTimePosition(new_position);
this.props.uiActions.createNotification({ content: 'fast-backward', type: 'shortcut', key: 'shortcut', duration: 1 });
} else if (e.ctrlKey || e.metaKey) {
} else if (e.altKey) {
this.props.mopidyActions.previous();
this.props.uiActions.createNotification({ content: 'step-backward', type: 'shortcut', key: 'shortcut', duration: 1 });
}
@ -64900,10 +64910,10 @@ var App = exports.App = function (_React$Component) {
case 39:
// right
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
this.props.mopidyActions.seek(this.props.play_time_position + 30000);
if (e.altKey && e.shiftKey) {
this.props.mopidyActions.setTimePosition(this.props.play_time_position + 30000);
this.props.uiActions.createNotification({ content: 'fast-forward', type: 'shortcut', key: 'shortcut', duration: 1 });
} else if (e.ctrlKey || e.metaKey) {
} else if (e.altKey) {
this.props.mopidyActions.next();
this.props.uiActions.createNotification({ content: 'step-forward', type: 'shortcut', key: 'shortcut', duration: 1 });
}
@ -64911,8 +64921,8 @@ var App = exports.App = function (_React$Component) {
case 70:
// F
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
window.history.push('/modal/kiosk-mode');
if (e.altKey && e.shiftKey) {
this.props.history.push('/kiosk-mode');
}
break;
}
@ -70655,7 +70665,6 @@ var Playlist = function (_React$Component) {
value: function render() {
var _this3 = this;
var scheme = helpers.uriSource(this.props.uri);
var playlist_id = helpers.getFromUri('playlistid', this.props.uri);
if (!this.props.playlist) {
@ -70687,7 +70696,7 @@ var Playlist = function (_React$Component) {
context = 'editable-playlist';
}
if (!playlist.tracks_uris || playlist.tracks_uris && !playlist.tracks || playlist.tracks_uris.length !== playlist.tracks.length) {
if (playlist.tracks_total !== 0 && (!playlist.tracks_uris || playlist.tracks_uris && !playlist.tracks || playlist.tracks_uris.length !== playlist.tracks.length)) {
var is_loading_tracks = true;
} else {
var is_loading_tracks = false;
@ -70728,13 +70737,13 @@ var Playlist = function (_React$Component) {
playlist.user ? playlist.user.name : helpers.getFromUri('userid', playlist.user_uri)
)
) : null,
playlist.tracks ? _react2.default.createElement(
_react2.default.createElement(
'li',
null,
playlist.tracks_total ? playlist.tracks_total : '0',
' tracks'
) : null,
!this.props.slim_mode && playlist.tracks ? _react2.default.createElement(
),
!this.props.slim_mode && playlist.tracks && playlist.tracks_total > 0 ? _react2.default.createElement(
'li',
null,
_react2.default.createElement(_Dater2.default, { type: 'total-time', data: playlist.tracks })
@ -70786,10 +70795,16 @@ var Playlist = function (_React$Component) {
var mapStateToProps = function mapStateToProps(state, ownProps) {
// Decode the URI, and then re-encode all the spaces
// This is needed as Mopidy encodes spaces in playlist URIs (but not other characters)
// Decode the URI, and then re-encode selected characters
// This is needed as Mopidy encodes *some* characters in playlist URIs (but not other characters)
// We need to retain ":" because this a reserved URI separator
var uri = decodeURIComponent(ownProps.match.params.uri);
uri = uri.replace(/\s/g, '%20');
uri = uri.replace(/\s/g, '%20'); // space
uri = uri.replace(/\[/g, '%5B'); // [
uri = uri.replace(/\]/g, '%5D'); // ]
uri = uri.replace(/\(/g, '%28'); // (
uri = uri.replace(/\)/g, '%29'); // )
uri = uri.replace(/\#/g, '%23'); // #
return {
uri: uri,
@ -83725,7 +83740,7 @@ var ListItem = function (_React$Component) {
value.id
);
if (key_string === 'popularity') return _react2.default.createElement(_Popularity2.default, { full: true, popularity: value });
if (key[0] === 'artists') return _react2.default.createElement(_ArtistSentence2.default, { artists: value });
if (key_string === 'artists') return _react2.default.createElement(_ArtistSentence2.default, { artists: value });
if (value === true) return _react2.default.createElement(_Icon2.default, { name: 'check' });
if (typeof value === 'number') return _react2.default.createElement(
'span',
@ -85690,6 +85705,8 @@ var EditPlaylist = function (_React$Component) {
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
// Playlist just loaded
if (!this.props.playlist && nextProps.playlist) {
this.setState({
name: nextProps.playlist.name,
@ -85697,6 +85714,10 @@ var EditPlaylist = function (_React$Component) {
public: nextProps.playlist.public == true,
collaborative: nextProps.playlist.collaborative == true
});
// Mopidy just connected, and we don't have the playlist yet
} else if (this.props.mopidy_connected != nextProps.mopidy_connected && !nextProps.playlist) {
this.props.mopidyActions.getPlaylist(this.props.uri);
}
}
}, {
@ -85923,10 +85944,16 @@ var EditPlaylist = function (_React$Component) {
var mapStateToProps = function mapStateToProps(state, ownProps) {
// Decode the URI, and then re-encode all the spaces
// This is needed as Mopidy encodes spaces in playlist URIs (but not other characters)
// Decode the URI, and then re-encode selected characters
// This is needed as Mopidy encodes *some* characters in playlist URIs (but not other characters)
// We need to retain ":" because this a reserved URI separator
var uri = decodeURIComponent(ownProps.match.params.uri);
uri = uri.replace(/\s/g, '%20');
uri = uri.replace(/\s/g, '%20'); // space
uri = uri.replace(/\[/g, '%5B'); // [
uri = uri.replace(/\]/g, '%5D'); // ]
uri = uri.replace(/\(/g, '%28'); // (
uri = uri.replace(/\)/g, '%29'); // )
uri = uri.replace(/\#/g, '%23'); // #
return {
uri: uri,

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

@ -98,7 +98,7 @@
// Release details
// These are automatically injected to built HTML
var build = "1554253014";
var build = "1554528127";
var version = "3.34.0";
// Construct the script tag

View File

@ -169,10 +169,10 @@ export class App extends React.Component{
}
shouldTriggerShortcut(e){
if (!this.props.shortkeys_enabled){
return false;
}
let keyCode = e.keyCode || e.which;
// When we're focussed on certian elements, don't fire any shortcuts
// Typically form inputs
@ -181,23 +181,20 @@ export class App extends React.Component{
return false;
}
// Listen for standalone key codes
let keyCodes = [27,32,191];
if (keyCodes.indexOf(e.keyCode) > -1){
let hotkeys = [27,32,191];
if (hotkeys.indexOf(keyCode) > -1){
e.preventDefault();
return true;
}
// Listen for key codes that require ctrl to be held
let keyCodesWithCtrl = [37,38,39,40];
if ((e.ctrlKey || e.metaKey) && keyCodesWithCtrl.indexOf(e.keyCode) > -1){
let hokeys_with_alt = [37,38,39,40];
if (e.altKey && hokeys_with_alt.indexOf(keyCode) > -1){
e.preventDefault();
return true;
}
// Listen for key codes that require ctrl to be held
let keyCodesWithCtrlShift = [70];
if ((e.ctrlKey || e.metaKey) && e.shiftKey && keyCodesWithCtrlShift.indexOf(e.keyCode) > -1){
let hokeys_with_alt_and_shift = [70];
if (e.altKey && e.shiftKey && hokeys_with_alt_and_shift.indexOf(keyCode) > -1){
e.preventDefault();
return true;
}
@ -241,12 +238,12 @@ export class App extends React.Component{
this.shouldTriggerShortcut(e)
}
handleKeyUp(e){
handleKeyUp(e){
if (!this.shouldTriggerShortcut(e)){
return
}
switch(e.keyCode){
switch(e.keyCode || e.which){
case 32: // spacebar
if (e.ctrlKey || e.metaKey){
@ -268,10 +265,10 @@ export class App extends React.Component{
break;
case 40: // down
if ((e.ctrlKey || e.metaKey) && e.shiftKey){
if (e.altKey && e.shiftKey){
this.props.mopidyActions.setMute(true);
this.props.uiActions.createNotification({content: 'volume-off', type: 'shortcut', key: 'shortcut', duration: 1});
} else if (e.ctrlKey){
} else if (e.altKey){
var volume = this.props.volume;
if (volume !== 'false'){
volume -= 5;
@ -288,13 +285,13 @@ export class App extends React.Component{
break;
case 38: // up
if ((e.ctrlKey || e.metaKey) && e.shiftKey){
if (e.altKey && e.shiftKey){
this.props.mopidyActions.setVolume(100)
if (this.props.mute){
this.props.mopidyActions.setMute(false);
}
this.props.uiActions.createNotification({content: 'volume-up', type: 'shortcut', key: 'shortcut', duration: 1});
} else if (e.ctrlKey || e.metaKey){
} else if (e.altKey){
var volume = this.props.volume
if (volume !== 'false'){
volume += 5;
@ -311,32 +308,32 @@ export class App extends React.Component{
break;
case 37: // left
if ((e.ctrlKey || e.metaKey) && e.shiftKey){
if (e.altKey && e.shiftKey){
var new_position = this.props.play_time_position - 30000;
if (new_position < 0){
new_position = 0;;
}
this.props.mopidyActions.seek(new_position);
this.props.mopidyActions.setTimePosition(new_position);
this.props.uiActions.createNotification({content: 'fast-backward', type: 'shortcut', key: 'shortcut', duration: 1});
} else if (e.ctrlKey || e.metaKey){
} else if (e.altKey){
this.props.mopidyActions.previous();
this.props.uiActions.createNotification({content: 'step-backward', type: 'shortcut', key: 'shortcut', duration: 1});
}
break;
case 39: // right
if ((e.ctrlKey || e.metaKey) && e.shiftKey){
this.props.mopidyActions.seek(this.props.play_time_position + 30000);
if (e.altKey && e.shiftKey){
this.props.mopidyActions.setTimePosition(this.props.play_time_position + 30000);
this.props.uiActions.createNotification({content: 'fast-forward', type: 'shortcut', key: 'shortcut', duration: 1});
} else if (e.ctrlKey || e.metaKey){
} else if (e.altKey){
this.props.mopidyActions.next();
this.props.uiActions.createNotification({content: 'step-forward', type: 'shortcut', key: 'shortcut', duration: 1});
}
break;
case 70: // F
if ((e.ctrlKey || e.metaKey) && e.shiftKey){
window.history.push('/modal/kiosk-mode');
if (e.altKey && e.shiftKey){
this.props.history.push('/kiosk-mode');
}
break;
}

View File

@ -15,6 +15,10 @@ export default class Icon extends React.Component{
}
render(){
if (!this.props.name || this.props.name === ""){
return null;
}
var className = "icon icon--"+(this.props.type ? this.props.type : 'material');
if (this.props.className){
className += ' '+this.props.className;

View File

@ -88,7 +88,7 @@ export default class ListItem extends React.Component{
if (key_string === 'added_at') return <span>Added <Dater type="ago" data={value} /> ago</span>
if (key_string === 'owner') return <URILink type="user" uri={value.uri}>{value.id}</URILink>
if (key_string === 'popularity') return <Popularity full popularity={value} />
if (key[0] === 'artists') return <ArtistSentence artists={value} />
if (key_string === 'artists') return <ArtistSentence artists={value} />
if (value === true) return <Icon name="check" />
if (typeof(value) === 'number') return <span>{value.toLocaleString()}</span>
return <span>{value}</span>

View File

@ -1271,9 +1271,13 @@ export let sortItems = function (array, property, reverse = false, sort_map = nu
for (var property_element of split){
// Apply sort on a property of the first item of an array
if (property_element == 'first' && Array.isArray(value) && value.length > 0){
value = value[0];
continue;
if (property_element == 'first'){
if (Array.isArray(value) && value.length > 0){
value = value[0];
continue;
} else {
return null;
}
// Just need the length of an array
} else if (property_element == 'length'){
@ -1323,7 +1327,9 @@ export let sortItems = function (array, property, reverse = false, sort_map = nu
// Alphabetic sorting
} else if (typeof(a_value) === 'string'){
if (!a_value || !b_value ) return 0;
if (a_value && !b_value ) return -1;
if (!a_value && b_value ) return 1;
if (!a_value && !b_value ) return 0;
if (a_value.toLowerCase() > b_value.toLowerCase()) return 1;
if (a_value.toLowerCase() < b_value.toLowerCase()) return -1;
return 0

View File

@ -322,9 +322,8 @@ export function getImages(context, uri){
.then(
response => {
if (response.album){
record = Object.assign({}, record, {images: response.album.image});
dispatch(coreActions.trackLoaded(record));
dispatch(coreActions.albumLoaded(response.album));
let images = helpers.formatImages(response.album.image);
dispatch(coreActions.trackLoaded({uri: uri, images: images}));
}
}
);

View File

@ -126,7 +126,7 @@ const PusherMiddleware = (function(){
break;
case 'local_scan_error':
store.dispatch(uiActions.processFinished('local_scan'));
store.dispatch(uiActions.createNotification({key: 'local_scan', type: 'bad', content: 'Local scan failed'}));
store.dispatch(uiActions.createNotification({type: 'bad', content: message.params.message, description: message.params.description}));
break;
// Upgrade
@ -138,7 +138,7 @@ const PusherMiddleware = (function(){
break;
case 'upgrade_error':
store.dispatch(uiActions.processFinished('upgrade'));
store.dispatch(uiActions.createNotification({type: 'bad', content: 'Upgrade failed'}));
store.dispatch(uiActions.createNotification({type: 'bad', content: message.params.message, description: message.params.description}));
break;
// Restart
@ -146,6 +146,10 @@ const PusherMiddleware = (function(){
store.dispatch(uiActions.processFinished('upgrade'));
store.dispatch(uiActions.createNotification({type: 'info', content: 'Restarting server...'}));
break;
case 'restart_error':
store.dispatch(uiActions.processFinished('upgrade'));
store.dispatch(uiActions.createNotification({type: 'bad', content: message.params.message, description: message.params.description}));
break;
// Test
case 'test_started':

View File

@ -178,7 +178,6 @@ class Playlist extends React.Component{
}
render(){
var scheme = helpers.uriSource(this.props.uri);
var playlist_id = helpers.getFromUri('playlistid',this.props.uri);
if (!this.props.playlist){
@ -204,7 +203,7 @@ class Playlist extends React.Component{
context = 'editable-playlist';
}
if (!playlist.tracks_uris || (playlist.tracks_uris && !playlist.tracks) || (playlist.tracks_uris.length !== playlist.tracks.length)){
if (playlist.tracks_total !== 0 && (!playlist.tracks_uris || (playlist.tracks_uris && !playlist.tracks) || (playlist.tracks_uris.length !== playlist.tracks.length))){
var is_loading_tracks = true;
} else {
var is_loading_tracks = false;
@ -226,8 +225,8 @@ class Playlist extends React.Component{
<ul className="details">
{!this.props.slim_mode ? <li className="source"><Icon type="fontawesome" name={helpers.sourceIcon(playlist.uri)} /></li> : null }
{playlist.user_uri ? <li><URILink type="user" uri={playlist.user_uri}>{playlist.user ? playlist.user.name : helpers.getFromUri('userid',playlist.user_uri)}</URILink></li> : null }
{playlist.tracks ? <li>{playlist.tracks_total ? playlist.tracks_total : '0'} tracks</li> : null }
{!this.props.slim_mode && playlist.tracks ? <li><Dater type="total-time" data={playlist.tracks} /></li> : null }
<li>{playlist.tracks_total ? playlist.tracks_total : '0'} tracks</li>
{!this.props.slim_mode && playlist.tracks && playlist.tracks_total > 0 ? <li><Dater type="total-time" data={playlist.tracks} /></li> : null }
{!this.props.slim_mode && playlist.followers !== undefined ? <li><NiceNumber value={playlist.followers} /> followers</li> : null }
{!this.props.slim_mode && playlist.last_modified_date ? <li>Edited <Dater type="ago" data={playlist.last_modified_date} /></li> : null }
</ul>
@ -257,10 +256,16 @@ class Playlist extends React.Component{
const mapStateToProps = (state, ownProps) => {
// Decode the URI, and then re-encode all the spaces
// This is needed as Mopidy encodes spaces in playlist URIs (but not other characters)
// Decode the URI, and then re-encode selected characters
// This is needed as Mopidy encodes *some* characters in playlist URIs (but not other characters)
// We need to retain ":" because this a reserved URI separator
var uri = decodeURIComponent(ownProps.match.params.uri);
uri = uri.replace(/\s/g, '%20');
uri = uri.replace(/\s/g, '%20'); // space
uri = uri.replace(/\[/g, '%5B'); // [
uri = uri.replace(/\]/g, '%5D'); // ]
uri = uri.replace(/\(/g, '%28'); // (
uri = uri.replace(/\)/g, '%29'); // )
uri = uri.replace(/\#/g, '%23'); // #
return {
uri: uri,

View File

@ -56,6 +56,8 @@ class EditPlaylist extends React.Component{
}
componentWillReceiveProps(nextProps){
// Playlist just loaded
if (!this.props.playlist && nextProps.playlist){
this.setState({
name: nextProps.playlist.name,
@ -63,6 +65,10 @@ class EditPlaylist extends React.Component{
public: (nextProps.playlist.public == true),
collaborative: (nextProps.playlist.collaborative == true)
});
// Mopidy just connected, and we don't have the playlist yet
} else if (this.props.mopidy_connected != nextProps.mopidy_connected && !nextProps.playlist){
this.props.mopidyActions.getPlaylist(this.props.uri);
}
}
@ -205,10 +211,16 @@ class EditPlaylist extends React.Component{
const mapStateToProps = (state, ownProps) => {
// Decode the URI, and then re-encode all the spaces
// This is needed as Mopidy encodes spaces in playlist URIs (but not other characters)
// Decode the URI, and then re-encode selected characters
// This is needed as Mopidy encodes *some* characters in playlist URIs (but not other characters)
// We need to retain ":" because this a reserved URI separator
var uri = decodeURIComponent(ownProps.match.params.uri);
uri = uri.replace(/\s/g, '%20');
uri = uri.replace(/\s/g, '%20'); // space
uri = uri.replace(/\[/g, '%5B'); // [
uri = uri.replace(/\]/g, '%5D'); // ]
uri = uri.replace(/\(/g, '%28'); // (
uri = uri.replace(/\)/g, '%29'); // )
uri = uri.replace(/\#/g, '%23'); // #
return {
uri: uri,