Removing vanilla encodeURIComponent, doing it manually for better control

This commit is contained in:
James Barnsley
2021-01-23 16:24:07 +13:00
parent 1bfc48e193
commit 4f561de7c5
22 changed files with 589 additions and 417 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -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}

View File

@ -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)}`}
/> />
)) ))
} }

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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);

View File

@ -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();
} }
} }

View File

@ -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)}
/> />
)) ))

View File

@ -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) {

View File

@ -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;
} }

View File

@ -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)));

View File

@ -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) {

View File

@ -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;*/
}; };
/** /**

View File

@ -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;
}; };

View File

@ -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>
); );

View File

@ -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>

View File

@ -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))(.*)`]);

View File

@ -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>
); );

View File

@ -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),
}); });
} }

View File

@ -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"