Removing vanilla encodeURIComponent, doing it manually for better control
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import GridItem from './GridItem';
|
import GridItem from './GridItem';
|
||||||
import { collate } from '../util/format';
|
import { collate, encodeUri } from '../util/format';
|
||||||
import * as uiActions from '../services/ui/actions';
|
import * as uiActions from '../services/ui/actions';
|
||||||
import * as lastfmActions from '../services/lastfm/actions';
|
import * as lastfmActions from '../services/lastfm/actions';
|
||||||
import * as spotifyActions from '../services/spotify/actions';
|
import * as spotifyActions from '../services/spotify/actions';
|
||||||
@ -47,7 +47,7 @@ class ArtistGrid extends React.Component {
|
|||||||
type="artist"
|
type="artist"
|
||||||
item={artist}
|
item={artist}
|
||||||
show_source_icon={show_source_icon}
|
show_source_icon={show_source_icon}
|
||||||
onClick={() => { history.push(`/artist/${encodeURIComponent(artist.uri)}`); }}
|
onClick={() => { history.push(`/artist/${encodeUri(artist.uri)}`); }}
|
||||||
lastfmActions={lastfmActions}
|
lastfmActions={lastfmActions}
|
||||||
spotifyActions={spotifyActions}
|
spotifyActions={spotifyActions}
|
||||||
spotifyAvailable={spotifyAvailable}
|
spotifyAvailable={spotifyAvailable}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import GridItem from './GridItem';
|
import GridItem from './GridItem';
|
||||||
|
import { encodeUri } from '../util/format';
|
||||||
|
|
||||||
export default memo(({
|
export default memo(({
|
||||||
categories,
|
categories,
|
||||||
@ -17,7 +18,7 @@ export default memo(({
|
|||||||
key={category.uri}
|
key={category.uri}
|
||||||
type="category"
|
type="category"
|
||||||
item={category}
|
item={category}
|
||||||
link={`/discover/categories/${encodeURIComponent(category.uri)}`}
|
link={`/discover/categories/${encodeUri(category.uri)}`}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import * as spotifyActions from '../../services/spotify/actions';
|
|||||||
import { arrayOf } from '../../util/arrays';
|
import { arrayOf } from '../../util/arrays';
|
||||||
import { decodeMopidyUri } from '../../util/helpers';
|
import { decodeMopidyUri } from '../../util/helpers';
|
||||||
import { i18n } from '../../locale';
|
import { i18n } from '../../locale';
|
||||||
|
import { encodeUri } from '../../util/format';
|
||||||
|
|
||||||
class Dropzones extends React.Component {
|
class Dropzones extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -64,7 +65,7 @@ class Dropzones extends React.Component {
|
|||||||
enqueueURIs(uris, from_uri, true);
|
enqueueURIs(uris, from_uri, true);
|
||||||
break;
|
break;
|
||||||
case 'add_to_playlist':
|
case 'add_to_playlist':
|
||||||
history.push(`/add-to-playlist/${encodeURIComponent(uris.join(','))}`);
|
history.push(`/add-to-playlist/${encodeUri(uris.join(','))}`);
|
||||||
// uris
|
// uris
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import * as uiActions from '../../services/ui/actions';
|
|||||||
import { uriType } from '../../util/helpers';
|
import { uriType } from '../../util/helpers';
|
||||||
import Icon from '../Icon';
|
import Icon from '../Icon';
|
||||||
import { i18n } from '../../locale';
|
import { i18n } from '../../locale';
|
||||||
|
import { encodeUri } from '../../util/format';
|
||||||
|
|
||||||
class SearchForm extends React.Component {
|
class SearchForm extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -64,19 +65,19 @@ class SearchForm extends React.Component {
|
|||||||
// check for uri type matching
|
// check for uri type matching
|
||||||
switch (uriType(term)) {
|
switch (uriType(term)) {
|
||||||
case 'album':
|
case 'album':
|
||||||
history.push(`/album/${encodeURIComponent(term)}`);
|
history.push(`/album/${encodeUri(term)}`);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'artist':
|
case 'artist':
|
||||||
history.push(`/artist/${encodeURIComponent(term)}`);
|
history.push(`/artist/${encodeUri(term)}`);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'playlist':
|
case 'playlist':
|
||||||
history.push(`/playlist/${encodeURIComponent(term)}`);
|
history.push(`/playlist/${encodeUri(term)}`);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'track':
|
case 'track':
|
||||||
history.push(`/track/${encodeURIComponent(term)}`);
|
history.push(`/track/${encodeUri(term)}`);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import Icon from './Icon';
|
|||||||
import Thumbnail from './Thumbnail';
|
import Thumbnail from './Thumbnail';
|
||||||
import LinksSentence from './LinksSentence';
|
import LinksSentence from './LinksSentence';
|
||||||
import { I18n } from '../locale';
|
import { I18n } from '../locale';
|
||||||
|
import { encodeUri } from '../util/format';
|
||||||
|
|
||||||
class GridItem extends React.Component {
|
class GridItem extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -114,7 +115,7 @@ class GridItem extends React.Component {
|
|||||||
if (!item) return null;
|
if (!item) return null;
|
||||||
if (album) item = { ...item, ...album };
|
if (album) item = { ...item, ...album };
|
||||||
|
|
||||||
const link = customLink || `/${type}/${encodeURIComponent(item.uri)}`;
|
const link = customLink || `/${type}/${encodeUri(item.uri)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, useHistory, useLocation } from 'react-router-dom';
|
import { Link, useHistory, useLocation } from 'react-router-dom';
|
||||||
import { scrollTo } from '../util/helpers';
|
import { scrollTo } from '../util/helpers';
|
||||||
@ -43,7 +42,6 @@ export default ({
|
|||||||
if (!retainScroll) scrollTo(scrollToProp, (scrollToProp));
|
if (!retainScroll) scrollTo(scrollToProp, (scrollToProp));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Decode both links. This handles issues where one link is encoded and the other isn't, but
|
// Decode both links. This handles issues where one link is encoded and the other isn't, but
|
||||||
// they're otherwise identical
|
// they're otherwise identical
|
||||||
const link = decodeURIComponent(to);
|
const link = decodeURIComponent(to);
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
scrollTo,
|
scrollTo,
|
||||||
} from '../util/helpers';
|
} from '../util/helpers';
|
||||||
import { I18n } from '../locale';
|
import { I18n } from '../locale';
|
||||||
|
import { encodeUri } from '../util/format';
|
||||||
|
|
||||||
export default class ListItem extends React.Component {
|
export default class ListItem extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -48,7 +49,7 @@ export default class ListItem extends React.Component {
|
|||||||
|
|
||||||
if (e.target.tagName.toLowerCase() !== 'a') {
|
if (e.target.tagName.toLowerCase() !== 'a') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
history.push(`${link_prefix}${encodeURIComponent(uri)}`);
|
history.push(`${link_prefix}${encodeUri(uri)}`);
|
||||||
scrollTo();
|
scrollTo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,7 +65,7 @@ export default class ListItem extends React.Component {
|
|||||||
|
|
||||||
if (e.target.tagName.toLowerCase() !== 'a') {
|
if (e.target.tagName.toLowerCase() !== 'a') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
history.push(`${link_prefix}${encodeURIComponent(uri)}`);
|
history.push(`${link_prefix}${encodeUri(uri)}`);
|
||||||
scrollTo();
|
scrollTo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
|||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import * as uiActions from '../services/ui/actions';
|
import * as uiActions from '../services/ui/actions';
|
||||||
import GridItem from './GridItem';
|
import GridItem from './GridItem';
|
||||||
|
import { encodeUri } from '../util/format';
|
||||||
|
|
||||||
class PlaylistGrid extends React.Component {
|
class PlaylistGrid extends React.Component {
|
||||||
handleContextMenu(e, item) {
|
handleContextMenu(e, item) {
|
||||||
@ -35,7 +36,7 @@ class PlaylistGrid extends React.Component {
|
|||||||
type="playlist"
|
type="playlist"
|
||||||
item={playlist}
|
item={playlist}
|
||||||
show_source_icon={this.props.show_source_icon}
|
show_source_icon={this.props.show_source_icon}
|
||||||
onClick={(e) => { this.props.history.push(`/playlist/${encodeURIComponent(playlist.uri)}`); }}
|
onClick={(e) => { this.props.history.push(`/playlist/${encodeUri(playlist.uri)}`); }}
|
||||||
onContextMenu={(e) => this.handleContextMenu(e, playlist)}
|
onContextMenu={(e) => this.handleContextMenu(e, playlist)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import Link from './Link';
|
import Link from './Link';
|
||||||
import { uriType as uriTypeHelper } from '../util/helpers';
|
import { uriType as uriTypeHelper } from '../util/helpers';
|
||||||
|
import { encodeUri } from '../util/format';
|
||||||
|
|
||||||
export default memo((props) => {
|
export default memo((props) => {
|
||||||
let to = null;
|
let to = null;
|
||||||
let { uri, type } = props;
|
let { uri, type } = props;
|
||||||
const uriType = type || uriTypeHelper(uri);
|
const uriType = type || uriTypeHelper(uri);
|
||||||
if (!props.unencoded) {
|
if (!props.unencoded) {
|
||||||
uri = encodeURIComponent(uri);
|
uri = encodeUri(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (uriType) {
|
switch (uriType) {
|
||||||
|
|||||||
@ -364,26 +364,27 @@ const CoreMiddleware = (function () {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'LOAD_URI': {
|
case 'LOAD_URIS': {
|
||||||
const { uri, options } = action;
|
const { uris, options } = action;
|
||||||
const source = uriSource(uri);
|
|
||||||
|
|
||||||
const fetch = () => {
|
uris.forEach((uri) => {
|
||||||
// We need to pull type from the URI for Spotify as we use specific HTTP endpoints for
|
const fetch = () => {
|
||||||
// each asset type, and their URIs facilitate this.
|
const source = uriSource(uri);
|
||||||
if (source === 'spotify') {
|
// We need to pull type from the URI for Spotify as we use specific HTTP endpoints for
|
||||||
store.dispatch(
|
// each asset type, and their URIs facilitate this.
|
||||||
coreActions[`load${titleCase(uriType(uri))}`](uri, options),
|
if (source === 'spotify') {
|
||||||
);
|
store.dispatch(
|
||||||
} else {
|
coreActions[`load${titleCase(uriType(uri))}`](uri, options),
|
||||||
mopidyActions.getUris([uri], options);
|
);
|
||||||
}
|
} else {
|
||||||
};
|
mopidyActions.getUris([uri], options);
|
||||||
|
}
|
||||||
ensureLoaded({
|
};
|
||||||
store,
|
ensureLoaded({
|
||||||
action,
|
store,
|
||||||
fetch,
|
action: { uri, options },
|
||||||
|
fetch,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1540,8 +1540,6 @@ const MopidyMiddleware = (function () {
|
|||||||
(results) => (results.length ? formatTrack(results[0]) : null),
|
(results) => (results.length ? formatTrack(results[0]) : null),
|
||||||
));
|
));
|
||||||
|
|
||||||
console.debug({ _response, tracks })
|
|
||||||
|
|
||||||
store.dispatch(coreActions.itemsLoaded(tracks));
|
store.dispatch(coreActions.itemsLoaded(tracks));
|
||||||
store.dispatch(mopidyActions.getImages(arrayOf('uri', tracks)));
|
store.dispatch(mopidyActions.getImages(arrayOf('uri', tracks)));
|
||||||
|
|
||||||
|
|||||||
@ -93,6 +93,8 @@ const removeDuplicates = function (array) {
|
|||||||
const applyFilter = function (field, value, array, singular = false) {
|
const applyFilter = function (field, value, array, singular = false) {
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
|
if (!array || !array.length) return results;
|
||||||
|
|
||||||
for (let i = 0; i < array.length; i++) {
|
for (let i = 0; i < array.length; i++) {
|
||||||
if (array[i][field] && String(array[i][field]).toLowerCase().includes(String(value).toLowerCase())) {
|
if (array[i][field] && String(array[i][field]).toLowerCase().includes(String(value).toLowerCase())) {
|
||||||
if (singular) {
|
if (singular) {
|
||||||
|
|||||||
@ -282,9 +282,15 @@ const formatSimpleObjects = function (records = []) {
|
|||||||
* '/' as this is a URL parameter delimiter
|
* '/' as this is a URL parameter delimiter
|
||||||
* @param {String} uri
|
* @param {String} uri
|
||||||
*/
|
*/
|
||||||
const encodeUri = (uri) => {
|
const encodeUri = (rawUri) => {
|
||||||
return encodeURIComponent(uri);
|
let uri = rawUri;
|
||||||
};
|
uri = uri.replace(/\//g, '%2F');
|
||||||
|
uri = uri.replace(/\?/g, '%3F');
|
||||||
|
|
||||||
|
return uri;
|
||||||
|
|
||||||
|
//encodeURIComponent(rawUri);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rebuild a URI with some ugly-ass handling of encoding.
|
* Rebuild a URI with some ugly-ass handling of encoding.
|
||||||
@ -301,6 +307,19 @@ const encodeUri = (uri) => {
|
|||||||
* @param {String} rawUri
|
* @param {String} rawUri
|
||||||
*/
|
*/
|
||||||
const decodeUri = (rawUri) => {
|
const decodeUri = (rawUri) => {
|
||||||
|
let uri = rawUri;
|
||||||
|
uri = uri.replace(/ /g, '%20');
|
||||||
|
uri = uri.replace(/%2F/g, '/');
|
||||||
|
uri = uri.replace(/%3F/g, '?');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What if we removed encoding completely? It looks like mopidy URIs are encoded to be URL friendly,
|
||||||
|
* except for slashes. So in principle we could just replace slash with %2F but otherwise not use
|
||||||
|
* encode/decodeUriComponent.
|
||||||
|
*/
|
||||||
|
|
||||||
|
return uri;
|
||||||
|
/*
|
||||||
let uri = decodeURIComponent(rawUri);
|
let uri = decodeURIComponent(rawUri);
|
||||||
uri = uri.replace(/,/g, '%2C');
|
uri = uri.replace(/,/g, '%2C');
|
||||||
uri = uri.replace(/'/g, '%27');
|
uri = uri.replace(/'/g, '%27');
|
||||||
@ -310,10 +329,11 @@ const decodeUri = (rawUri) => {
|
|||||||
uri = uri.replace(/\[/g, '%5B');
|
uri = uri.replace(/\[/g, '%5B');
|
||||||
uri = uri.replace(/\]/g, '%5D');
|
uri = uri.replace(/\]/g, '%5D');
|
||||||
uri = uri.replace(/ /g, '%20');
|
uri = uri.replace(/ /g, '%20');
|
||||||
|
uri = uri.replace(/#/g, '%23');
|
||||||
//uri = uri.replace(/\//g, '%2F');
|
//uri = uri.replace(/\//g, '%2F');
|
||||||
|
|
||||||
console.debug(uri);
|
console.debug({ rawUri, uri });
|
||||||
return uri;
|
return uri;*/
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { indexToArray } from "./arrays";
|
import { indexToArray } from "./arrays";
|
||||||
|
import { encodeUri } from "./format";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a function, that, as long as it continues to be invoked, will not
|
* Returns a function, that, as long as it continues to be invoked, will not
|
||||||
@ -339,8 +340,7 @@ const buildLink = (uri, type = null) => {
|
|||||||
// Encode the whole URI as though it's a component. This makes it URL friendly for
|
// Encode the whole URI as though it's a component. This makes it URL friendly for
|
||||||
// all Mopidy backends (some use URIs like local:track:http://rss.com/stuff.mp3) which
|
// all Mopidy backends (some use URIs like local:track:http://rss.com/stuff.mp3) which
|
||||||
// is never going to work nicely.
|
// is never going to work nicely.
|
||||||
link += encodeURIComponent(uri);
|
link += encodeUri(uri);
|
||||||
console.log({ type, link });
|
|
||||||
return link;
|
return link;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import Button from '../components/Button';
|
|||||||
import { makeLoadingSelector, makeItemSelector } from '../util/selectors';
|
import { makeLoadingSelector, makeItemSelector } from '../util/selectors';
|
||||||
import { applyFilter, sortItems } from '../util/arrays';
|
import { applyFilter, sortItems } from '../util/arrays';
|
||||||
import { trackEvent } from '../components/Trackable';
|
import { trackEvent } from '../components/Trackable';
|
||||||
|
import { encodeUri } from '../util/format';
|
||||||
|
|
||||||
class Album extends React.Component {
|
class Album extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -218,7 +219,7 @@ class Album extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<ErrorMessage type="not-found" title="Not found">
|
<ErrorMessage type="not-found" title="Not found">
|
||||||
<p>
|
<p>
|
||||||
{i18n('errors.uri_not_found', { uri: encodeURIComponent(uri) })}
|
{i18n('errors.uri_not_found', { uri })}
|
||||||
</p>
|
</p>
|
||||||
</ErrorMessage>
|
</ErrorMessage>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import {
|
|||||||
sourceIcon,
|
sourceIcon,
|
||||||
titleCase,
|
titleCase,
|
||||||
} from '../util/helpers';
|
} from '../util/helpers';
|
||||||
import { collate } from '../util/format';
|
import { collate, encodeUri } from '../util/format';
|
||||||
import { sortItems, applyFilter, arrayOf } from '../util/arrays';
|
import { sortItems, applyFilter, arrayOf } from '../util/arrays';
|
||||||
import { i18n, I18n } from '../locale';
|
import { i18n, I18n } from '../locale';
|
||||||
import Button from '../components/Button';
|
import Button from '../components/Button';
|
||||||
@ -265,7 +265,7 @@ class Artist extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
to={`/artist/${encodeURIComponent(uri)}/related-artists`}
|
to={`/artist/${encodeUri(uri)}/related-artists`}
|
||||||
scrollTo="#sub-views-menu"
|
scrollTo="#sub-views-menu"
|
||||||
>
|
>
|
||||||
<I18n path="artist.overview.related_artists.more" />
|
<I18n path="artist.overview.related_artists.more" />
|
||||||
@ -492,7 +492,7 @@ class Artist extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<ErrorMessage type="not-found" title="Not found">
|
<ErrorMessage type="not-found" title="Not found">
|
||||||
<p>
|
<p>
|
||||||
<I18n path="errors.uri_not_found" uri={encodeURIComponent(uri)} />
|
<I18n path="errors.uri_not_found" uri={uri} />
|
||||||
</p>
|
</p>
|
||||||
</ErrorMessage>
|
</ErrorMessage>
|
||||||
);
|
);
|
||||||
@ -539,7 +539,7 @@ class Artist extends React.Component {
|
|||||||
history={history}
|
history={history}
|
||||||
activeClassName="sub-views__option--active"
|
activeClassName="sub-views__option--active"
|
||||||
className="sub-views__option"
|
className="sub-views__option"
|
||||||
to={`/artist/${encodeURIComponent(uri)}`}
|
to={`/artist/${encodeUri(uri)}`}
|
||||||
scrollTo="#sub-views-menu"
|
scrollTo="#sub-views-menu"
|
||||||
>
|
>
|
||||||
<h4><I18n path="artist.overview.title" /></h4>
|
<h4><I18n path="artist.overview.title" /></h4>
|
||||||
@ -550,7 +550,7 @@ class Artist extends React.Component {
|
|||||||
history={history}
|
history={history}
|
||||||
activeClassName="sub-views__option--active"
|
activeClassName="sub-views__option--active"
|
||||||
className="sub-views__option"
|
className="sub-views__option"
|
||||||
to={`/artist/${encodeURIComponent(uri)}/tracks`}
|
to={`/artist/${encodeUri(uri)}/tracks`}
|
||||||
scrollTo="#sub-views-menu"
|
scrollTo="#sub-views-menu"
|
||||||
>
|
>
|
||||||
<h4><I18n path="artist.tracks.title" /></h4>
|
<h4><I18n path="artist.tracks.title" /></h4>
|
||||||
@ -562,7 +562,7 @@ class Artist extends React.Component {
|
|||||||
history={history}
|
history={history}
|
||||||
activeClassName="sub-views__option--active"
|
activeClassName="sub-views__option--active"
|
||||||
className="sub-views__option"
|
className="sub-views__option"
|
||||||
to={`/artist/${encodeURIComponent(uri)}/related-artists`}
|
to={`/artist/${encodeUri(uri)}/related-artists`}
|
||||||
scrollTo="#sub-views-menu"
|
scrollTo="#sub-views-menu"
|
||||||
>
|
>
|
||||||
<h4><I18n path="artist.related_artists.title" /></h4>
|
<h4><I18n path="artist.related_artists.title" /></h4>
|
||||||
@ -573,7 +573,7 @@ class Artist extends React.Component {
|
|||||||
history={history}
|
history={history}
|
||||||
activeClassName="sub-views__option--active"
|
activeClassName="sub-views__option--active"
|
||||||
className="sub-views__option"
|
className="sub-views__option"
|
||||||
to={`/artist/${encodeURIComponent(uri)}/about`}
|
to={`/artist/${encodeUri(uri)}/about`}
|
||||||
scrollTo="#sub-views-menu"
|
scrollTo="#sub-views-menu"
|
||||||
>
|
>
|
||||||
<h4><I18n path="artist.about.title" /></h4>
|
<h4><I18n path="artist.about.title" /></h4>
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import { trackEvent } from '../components/Trackable';
|
|||||||
import { i18n, I18n } from '../locale';
|
import { i18n, I18n } from '../locale';
|
||||||
import { makeItemSelector, makeLoadingSelector } from '../util/selectors';
|
import { makeItemSelector, makeLoadingSelector } from '../util/selectors';
|
||||||
import { sortItems, applyFilter } from '../util/arrays';
|
import { sortItems, applyFilter } from '../util/arrays';
|
||||||
|
import { decodeUri, encodeUri } from '../util/format';
|
||||||
|
|
||||||
class Playlist extends React.Component {
|
class Playlist extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -45,7 +46,7 @@ class Playlist extends React.Component {
|
|||||||
// We accept the old format, and redirect to the new one
|
// We accept the old format, and redirect to the new one
|
||||||
if (uri.includes('spotify:user:')) {
|
if (uri.includes('spotify:user:')) {
|
||||||
uri = uri.replace(/spotify:user:([^:]*?):/i, 'spotify:');
|
uri = uri.replace(/spotify:user:([^:]*?):/i, 'spotify:');
|
||||||
props.history.push(`/playlist/${encodeURIComponent(uri)}`);
|
props.history.push(`/playlist/${encodeUri(uri)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +77,7 @@ class Playlist extends React.Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (prevPlaylist && playlist && prevPlaylist.moved_to !== playlist.moved_to) {
|
if (prevPlaylist && playlist && prevPlaylist.moved_to !== playlist.moved_to) {
|
||||||
push(`/playlist/${encodeURIComponent(playlist.moved_to)}`);
|
push(`/playlist/${encodeUri(playlist.moved_to)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uri !== prevUri) {
|
if (uri !== prevUri) {
|
||||||
@ -258,7 +259,7 @@ class Playlist extends React.Component {
|
|||||||
<I18n path="actions.play" />
|
<I18n path="actions.play" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
to={`/playlist/${encodeURIComponent(uri)}/edit`}
|
to={`/playlist/${encodeUri(uri)}/edit`}
|
||||||
tracking={{ category: 'Playlist', action: 'Edit' }}
|
tracking={{ category: 'Playlist', action: 'Edit' }}
|
||||||
>
|
>
|
||||||
<I18n path="actions.edit" />
|
<I18n path="actions.edit" />
|
||||||
@ -280,7 +281,7 @@ class Playlist extends React.Component {
|
|||||||
<I18n path="actions.play" />
|
<I18n path="actions.play" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
to={`/playlist/${encodeURIComponent(uri)}/edit`}
|
to={`/playlist/${encodeUri(uri)}/edit`}
|
||||||
tracking={{ category: 'Playlist', action: 'Edit' }}
|
tracking={{ category: 'Playlist', action: 'Edit' }}
|
||||||
>
|
>
|
||||||
<I18n path="actions.edit" />
|
<I18n path="actions.edit" />
|
||||||
@ -495,7 +496,7 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
} = {},
|
} = {},
|
||||||
} = state;
|
} = state;
|
||||||
|
|
||||||
const uri = decodeMopidyUri(ownProps.match.params.uri);
|
const uri = decodeUri(ownProps.match.params.uri);
|
||||||
const playlistId = getFromUri('playlistid', uri);
|
const playlistId = getFromUri('playlistid', uri);
|
||||||
const itemSelector = makeItemSelector(uri);
|
const itemSelector = makeItemSelector(uri);
|
||||||
const loadingSelector = makeLoadingSelector([`(.*)${playlistId}(?!.*(following))(.*)`]);
|
const loadingSelector = makeLoadingSelector([`(.*)${playlistId}(?!.*(following))(.*)`]);
|
||||||
|
|||||||
@ -18,15 +18,121 @@ import * as mopidyActions from '../services/mopidy/actions';
|
|||||||
import * as spotifyActions from '../services/spotify/actions';
|
import * as spotifyActions from '../services/spotify/actions';
|
||||||
import * as lastfmActions from '../services/lastfm/actions';
|
import * as lastfmActions from '../services/lastfm/actions';
|
||||||
import * as geniusActions from '../services/genius/actions';
|
import * as geniusActions from '../services/genius/actions';
|
||||||
import {
|
import { sourceIcon } from '../util/helpers';
|
||||||
isLoading,
|
|
||||||
sourceIcon,
|
|
||||||
} from '../util/helpers';
|
|
||||||
import { i18n, I18n } from '../locale';
|
import { i18n, I18n } from '../locale';
|
||||||
import Button from '../components/Button';
|
import Button from '../components/Button';
|
||||||
import { makeLoadingSelector, makeItemSelector } from '../util/selectors';
|
import { makeLoadingSelector, makeItemSelector } from '../util/selectors';
|
||||||
import { decodeUri } from '../util/format';
|
import { decodeUri } from '../util/format';
|
||||||
|
|
||||||
|
const LyricsSelector = ({
|
||||||
|
track: {
|
||||||
|
uri,
|
||||||
|
lyrics_results,
|
||||||
|
lyrics_path,
|
||||||
|
} = {},
|
||||||
|
getTrackLyrics,
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
if (lyrics_results === undefined || lyrics_results === null) return null;
|
||||||
|
if (lyrics_results.length <= 0) {
|
||||||
|
return (
|
||||||
|
<div className="field lyrics-selector">
|
||||||
|
<div className="input">
|
||||||
|
<input type="text" disabled="disabled" value="No results" />
|
||||||
|
<div className="description">
|
||||||
|
<I18n path="services.genius.switch_lyrics_result" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="field lyrics-selector">
|
||||||
|
<div className="input">
|
||||||
|
<SelectField
|
||||||
|
onChange={(value) => getTrackLyrics(uri, value)}
|
||||||
|
options={
|
||||||
|
lyrics_results.map((result) => ({
|
||||||
|
value: result.path,
|
||||||
|
label: result.title,
|
||||||
|
defaultValue: (result.path === lyrics_path),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="description">
|
||||||
|
<I18n path="services.genius.switch_lyrics_result" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LyricsContent = ({
|
||||||
|
authorized,
|
||||||
|
loading,
|
||||||
|
track: {
|
||||||
|
lyrics,
|
||||||
|
lyrics_path,
|
||||||
|
} = {},
|
||||||
|
}) => {
|
||||||
|
if (loading) return <Loader mini />;
|
||||||
|
if (!lyrics && !authorized) {
|
||||||
|
return (
|
||||||
|
<p className="no-results">
|
||||||
|
<I18n path="track.want_lyrics" />
|
||||||
|
<Link to="/settings/services/genius" scrollTo="#services-menu">
|
||||||
|
<I18n path="settings.title" />
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
if (!lyrics) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="lyrics">
|
||||||
|
<div className="content" dangerouslySetInnerHTML={{ __html: lyrics }} />
|
||||||
|
<div className="origin mid_grey-text">
|
||||||
|
<I18n path="track.lyrics_origin" />
|
||||||
|
<a
|
||||||
|
href={`https://genius.com${lyrics_path}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
{`https://genius.com${lyrics_path}`}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Lyrics = ({
|
||||||
|
loading,
|
||||||
|
authorized,
|
||||||
|
track,
|
||||||
|
getTrackLyrics,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4>
|
||||||
|
<I18n path="track.lyrics" />
|
||||||
|
{loading && <Loader loading mini />}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<LyricsSelector
|
||||||
|
getTrackLyrics={getTrackLyrics}
|
||||||
|
authorized={authorized}
|
||||||
|
track={track}
|
||||||
|
/>
|
||||||
|
<LyricsContent
|
||||||
|
track={track}
|
||||||
|
authorized={authorized}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class Track extends React.Component {
|
class Track extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {
|
const {
|
||||||
@ -85,11 +191,13 @@ class Track extends React.Component {
|
|||||||
|
|
||||||
if (track) {
|
if (track) {
|
||||||
let artists = '';
|
let artists = '';
|
||||||
for (let i = 0; i < track.artists.length; i++) {
|
if (artists) {
|
||||||
if (artists != '') {
|
for (let i = 0; i < track.artists.length; i++) {
|
||||||
artists += ', ';
|
if (artists != '') {
|
||||||
|
artists += ', ';
|
||||||
|
}
|
||||||
|
artists += track.artists[i].name;
|
||||||
}
|
}
|
||||||
artists += track.artists[i].name;
|
|
||||||
}
|
}
|
||||||
setWindowTitle(i18n('track.title_window', { name: track.name, artists }));
|
setWindowTitle(i18n('track.title_window', { name: track.name, artists }));
|
||||||
} else {
|
} else {
|
||||||
@ -121,80 +229,6 @@ class Track extends React.Component {
|
|||||||
playURIs([uri], uri);
|
playURIs([uri], uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLyricsSelector = () => {
|
|
||||||
const {
|
|
||||||
track,
|
|
||||||
geniusActions: {
|
|
||||||
getTrackLyrics,
|
|
||||||
},
|
|
||||||
genius_authorized,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!genius_authorized || track.lyrics_results === undefined || track.lyrics_results === null) {
|
|
||||||
return null;
|
|
||||||
} if (track.lyrics_results.length <= 0) {
|
|
||||||
return (
|
|
||||||
<div className="field lyrics-selector">
|
|
||||||
<div className="input">
|
|
||||||
<input type="text" disabled="disabled" value="No results" />
|
|
||||||
<div className="description">
|
|
||||||
<I18n path="services.genius.switch_lyrics_result" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="field lyrics-selector">
|
|
||||||
<div className="input">
|
|
||||||
<SelectField
|
|
||||||
onChange={(value) => getTrackLyrics(track.uri, value)}
|
|
||||||
options={
|
|
||||||
track.lyrics_results.map((result) => ({
|
|
||||||
value: result.path,
|
|
||||||
label: result.title,
|
|
||||||
defaultValue: (result.path === track.lyrics_path),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className="description">
|
|
||||||
<I18n path="services.genius.switch_lyrics_result" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderLyrics = () => {
|
|
||||||
const {
|
|
||||||
track: {
|
|
||||||
lyrics,
|
|
||||||
lyrics_path,
|
|
||||||
} = {},
|
|
||||||
genius_authorized,
|
|
||||||
loadingLyrics,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!lyrics || !genius_authorized || loadingLyrics) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="lyrics">
|
|
||||||
<div className="content" dangerouslySetInnerHTML={{ __html: lyrics }} />
|
|
||||||
<div className="origin mid_grey-text">
|
|
||||||
<I18n path="track.lyrics_origin" />
|
|
||||||
<a
|
|
||||||
href={`https://genius.com${lyrics_path}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
>
|
|
||||||
{`https://genius.com${lyrics_path}`}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render = () => {
|
render = () => {
|
||||||
const {
|
const {
|
||||||
uri,
|
uri,
|
||||||
@ -203,6 +237,9 @@ class Track extends React.Component {
|
|||||||
slim_mode,
|
slim_mode,
|
||||||
uiActions,
|
uiActions,
|
||||||
genius_authorized,
|
genius_authorized,
|
||||||
|
geniusActions: {
|
||||||
|
getTrackLyrics
|
||||||
|
},
|
||||||
loadingLyrics,
|
loadingLyrics,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@ -288,22 +325,12 @@ class Track extends React.Component {
|
|||||||
<ContextMenuTrigger onTrigger={this.handleContextMenu} />
|
<ContextMenuTrigger onTrigger={this.handleContextMenu} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4>
|
<Lyrics
|
||||||
<I18n path="track.lyrics" />
|
loading={loadingLyrics}
|
||||||
{loadingLyrics && <Loader loading mini />}
|
authorized={genius_authorized}
|
||||||
</h4>
|
getTrackLyrics={getTrackLyrics}
|
||||||
|
track={track}
|
||||||
{!genius_authorized && (
|
/>
|
||||||
<p className="no-results">
|
|
||||||
<I18n path="track.want_lyrics" />
|
|
||||||
<Link to="/settings/services/genius" scrollTo="#services-menu">
|
|
||||||
<I18n path="settings.title" />
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{this.renderLyricsSelector()}
|
|
||||||
{this.renderLyrics()}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import Icon from '../../components/Icon';
|
|||||||
import ErrorBoundary from '../../components/ErrorBoundary';
|
import ErrorBoundary from '../../components/ErrorBoundary';
|
||||||
import * as uiActions from '../../services/ui/actions';
|
import * as uiActions from '../../services/ui/actions';
|
||||||
import * as mopidyActions from '../../services/mopidy/actions';
|
import * as mopidyActions from '../../services/mopidy/actions';
|
||||||
import { formatImages } from '../../util/format';
|
import { formatImages, encodeUri } from '../../util/format';
|
||||||
import { I18n, i18n } from '../../locale';
|
import { I18n, i18n } from '../../locale';
|
||||||
|
|
||||||
class LibraryBrowse extends React.Component {
|
class LibraryBrowse extends React.Component {
|
||||||
@ -91,7 +91,7 @@ class LibraryBrowse extends React.Component {
|
|||||||
|
|
||||||
grid_items.push({
|
grid_items.push({
|
||||||
name: subdirectory.name,
|
name: subdirectory.name,
|
||||||
link: `/library/browse/${encodeURIComponent(subdirectory.uri)}`,
|
link: `/library/browse/${encodeUri(subdirectory.uri)}`,
|
||||||
icons: formatImages(subdirectory.icons),
|
icons: formatImages(subdirectory.icons),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
@ -8,6 +7,7 @@ import List from '../../components/List';
|
|||||||
import TrackList from '../../components/TrackList';
|
import TrackList from '../../components/TrackList';
|
||||||
import GridItem from '../../components/GridItem';
|
import GridItem from '../../components/GridItem';
|
||||||
import DropdownField from '../../components/Fields/DropdownField';
|
import DropdownField from '../../components/Fields/DropdownField';
|
||||||
|
import FilterField from '../../components/Fields/FilterField';
|
||||||
import Icon from '../../components/Icon';
|
import Icon from '../../components/Icon';
|
||||||
import URILink from '../../components/URILink';
|
import URILink from '../../components/URILink';
|
||||||
import ErrorBoundary from '../../components/ErrorBoundary';
|
import ErrorBoundary from '../../components/ErrorBoundary';
|
||||||
@ -18,9 +18,10 @@ import * as spotifyActions from '../../services/spotify/actions';
|
|||||||
import {
|
import {
|
||||||
isLoading,
|
isLoading,
|
||||||
} from '../../util/helpers';
|
} from '../../util/helpers';
|
||||||
import { arrayOf, sortItems } from '../../util/arrays';
|
import { arrayOf, sortItems, applyFilter } from '../../util/arrays';
|
||||||
import { i18n, I18n } from '../../locale';
|
import { i18n, I18n } from '../../locale';
|
||||||
import Button from '../../components/Button';
|
import Button from '../../components/Button';
|
||||||
|
import { encodeUri } from '../../util/format';
|
||||||
|
|
||||||
class LibraryBrowseDirectory extends React.Component {
|
class LibraryBrowseDirectory extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -153,7 +154,7 @@ class LibraryBrowseDirectory extends React.Component {
|
|||||||
<GridItem
|
<GridItem
|
||||||
key={subdirectory.uri}
|
key={subdirectory.uri}
|
||||||
type="directory"
|
type="directory"
|
||||||
link={`/library/browse/${encodeURIComponent(subdirectory.uri)}`}
|
link={`/library/browse/${encodeUri(subdirectory.uri)}`}
|
||||||
item={subdirectory}
|
item={subdirectory}
|
||||||
nocontext
|
nocontext
|
||||||
/>
|
/>
|
||||||
@ -172,6 +173,8 @@ class LibraryBrowseDirectory extends React.Component {
|
|||||||
view,
|
view,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const {
|
||||||
|
filter,
|
||||||
|
per_page,
|
||||||
limit,
|
limit,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
@ -198,6 +201,9 @@ class LibraryBrowseDirectory extends React.Component {
|
|||||||
|
|
||||||
let subdirectories = (directory.subdirectories && directory.subdirectories.length > 0 ? directory.subdirectories : null);
|
let subdirectories = (directory.subdirectories && directory.subdirectories.length > 0 ? directory.subdirectories : null);
|
||||||
subdirectories = sortItems(subdirectories, 'name');
|
subdirectories = sortItems(subdirectories, 'name');
|
||||||
|
if (filter && filter !== '') {
|
||||||
|
subdirectories = applyFilter('name', filter, subdirectories);
|
||||||
|
}
|
||||||
|
|
||||||
const total_items = (directory.tracks ? directory.tracks.length : 0) + (subdirectories ? subdirectories.length : 0);
|
const total_items = (directory.tracks ? directory.tracks.length : 0) + (subdirectories ? subdirectories.length : 0);
|
||||||
subdirectories = subdirectories.slice(0, limit);
|
subdirectories = subdirectories.slice(0, limit);
|
||||||
@ -207,6 +213,9 @@ class LibraryBrowseDirectory extends React.Component {
|
|||||||
if (limit_remaining > 0 && directory.tracks && directory.tracks.length) {
|
if (limit_remaining > 0 && directory.tracks && directory.tracks.length) {
|
||||||
all_tracks = directory.tracks;
|
all_tracks = directory.tracks;
|
||||||
all_tracks = sortItems(all_tracks, 'name');
|
all_tracks = sortItems(all_tracks, 'name');
|
||||||
|
if (filter && filter !== '') {
|
||||||
|
tracks = applyFilter('name', filter, tracks);
|
||||||
|
}
|
||||||
tracks = all_tracks.slice(0, limit_remaining);
|
tracks = all_tracks.slice(0, limit_remaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,6 +232,11 @@ class LibraryBrowseDirectory extends React.Component {
|
|||||||
|
|
||||||
const options = (
|
const options = (
|
||||||
<>
|
<>
|
||||||
|
<FilterField
|
||||||
|
initialValue={filter}
|
||||||
|
handleChange={(value) => this.setState({ filter: value, limit: per_page })}
|
||||||
|
onSubmit={() => uiActions.hideContextMenu()}
|
||||||
|
/>
|
||||||
<DropdownField
|
<DropdownField
|
||||||
icon="visibility"
|
icon="visibility"
|
||||||
name="View"
|
name="View"
|
||||||
|
|||||||
Reference in New Issue
Block a user