All views done this time; Language file mapping for modals.. Will need to do destructuring in second sweep
This commit is contained in:
@ -3,10 +3,12 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import LinksSentence from '../LinksSentence';
|
||||
import TextField from './TextField';
|
||||
import * as coreActions from '../../services/core/actions';
|
||||
import * as uiActions from '../../services/ui/actions';
|
||||
import * as spotifyActions from '../../services/spotify/actions';
|
||||
import { generateGuid } from '../../util/helpers';
|
||||
import { i18n } from '../../locale';
|
||||
|
||||
class AddSeedField extends React.Component {
|
||||
constructor(props) {
|
||||
@ -21,10 +23,17 @@ class AddSeedField extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
genres,
|
||||
spotifyActions: {
|
||||
getGenres,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
window.addEventListener('click', this.handleClick, false);
|
||||
|
||||
if (!this.props.genres) {
|
||||
this.props.spotifyActions.getGenres();
|
||||
if (!genres) {
|
||||
getGenres();
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,13 +41,24 @@ class AddSeedField extends React.Component {
|
||||
window.removeEventListener('click', this.handleClick, false);
|
||||
}
|
||||
|
||||
handleClick(e) {
|
||||
handleClick = (e) => {
|
||||
const {
|
||||
spotifyActions: {
|
||||
clearAutocompleteResults,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
if ($(e.target).closest('.add-seed-field').length <= 0) {
|
||||
this.props.spotifyActions.clearAutocompleteResults(this.id);
|
||||
clearAutocompleteResults(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(e, value) {
|
||||
handleChange = (value) => {
|
||||
const {
|
||||
spotifyActions: {
|
||||
getAutocompleteResults,
|
||||
},
|
||||
} = this.props;
|
||||
const self = this;
|
||||
|
||||
// update our local state
|
||||
@ -49,68 +69,85 @@ class AddSeedField extends React.Component {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(
|
||||
() => {
|
||||
self.setState({ searching: true });
|
||||
self.props.spotifyActions.getAutocompleteResults(self.id, value, ['artist', 'track', 'genre']);
|
||||
self.setState({ searching: true });
|
||||
getAutocompleteResults(self.id, value, ['artist', 'track', 'genre']);
|
||||
},
|
||||
500,
|
||||
);
|
||||
}
|
||||
|
||||
handleSelect(e, item) {
|
||||
handleSelect = (e, item) => {
|
||||
const {
|
||||
onSelect,
|
||||
spotifyActions: {
|
||||
clearAutocompleteResults,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
this.setState({ value: '' });
|
||||
this.props.onSelect(e, item.uri);
|
||||
this.props.spotifyActions.clearAutocompleteResults(this.id);
|
||||
onSelect(e, item.uri);
|
||||
clearAutocompleteResults(this.id);
|
||||
}
|
||||
|
||||
results() {
|
||||
if (this.props.results === undefined) {
|
||||
return null;
|
||||
} if (this.props.results[this.id] === undefined) {
|
||||
results = (type) => {
|
||||
const {
|
||||
results: resultsProp,
|
||||
} = this.props;
|
||||
|
||||
const results = resultsProp[this.id];
|
||||
if (type) {
|
||||
if (results) {
|
||||
return results[type];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return this.props.results[this.id];
|
||||
return results;
|
||||
}
|
||||
|
||||
renderResults(type) {
|
||||
const results = this.results();
|
||||
if (!results || typeof (results[type]) === 'undefined' || results[type].length <= 0) return null;
|
||||
|
||||
// only show the first 3
|
||||
const items = results[type].slice(0, 3);
|
||||
renderResults = (type) => {
|
||||
const results = this.results(type);
|
||||
if (!results) return null;
|
||||
|
||||
return (
|
||||
<div className="type">
|
||||
<h4 className="mid_grey-text">{type}</h4>
|
||||
{
|
||||
items.map((item) => (
|
||||
<div className="result" key={item.uri} onClick={(e) => this.handleSelect(e, item)}>
|
||||
{item.name}
|
||||
{type == 'tracks' ? (
|
||||
<span className="mid_grey-text">
|
||||
{' '}
|
||||
<LinksSentence items={item.artists} nolinks />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
results.slice(0, 3).map((item) => (
|
||||
<div className="result" key={item.uri} onClick={(e) => this.handleSelect(e, item)}>
|
||||
{item.name}
|
||||
{type === 'tracks' && (
|
||||
<span className="mid_grey-text">
|
||||
{' '}
|
||||
<LinksSentence items={item.artists} nolinks />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
render = () => {
|
||||
const {
|
||||
placeholder,
|
||||
className: classNameProp = '',
|
||||
} = this.props;
|
||||
const { value } = this.state;
|
||||
const results = this.results();
|
||||
|
||||
let className = 'field autocomplete-field add-seed-field';
|
||||
if (this.results() && this.results().loading) {
|
||||
if (results && results.loading) {
|
||||
className += ' loading';
|
||||
}
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={`${className} ${classNameProp}`}>
|
||||
<div className="input">
|
||||
<input
|
||||
type="text"
|
||||
value={this.state.value}
|
||||
onChange={(e) => this.handleChange(e, e.target.value)}
|
||||
placeholder={this.props.placeholder ? this.props.placeholder : 'Start typing...'}
|
||||
<TextField
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
placeholder={placeholder || i18n('fields.add_seed_field.placeholder')}
|
||||
everyChange
|
||||
/>
|
||||
</div>
|
||||
<div className="results">
|
||||
@ -123,9 +160,9 @@ class AddSeedField extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
genres: (state.spotify.genres ? state.spotify.genres : null),
|
||||
results: (state.spotify.autocomplete_results ? state.spotify.autocomplete_results : {}),
|
||||
const mapStateToProps = (state) => ({
|
||||
genres: (state.spotify.genres || null),
|
||||
results: (state.spotify.autocomplete_results || {}),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
@ -22,8 +22,13 @@ export default class TextField extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
handleChange = (e) => {
|
||||
this.setState({ value: e.target.value });
|
||||
handleChange = ({ target: { value } }) => {
|
||||
const { everyChange, onChange } = this.props;
|
||||
|
||||
this.setState({ value });
|
||||
if (everyChange) {
|
||||
onChange(value);
|
||||
}
|
||||
}
|
||||
|
||||
handleFocus = () => {
|
||||
|
||||
@ -3,6 +3,8 @@ errors:
|
||||
could_not_load: Could not load
|
||||
no_results: No results found
|
||||
authorization_required: 'You must authorize %{provider} first'
|
||||
enable_first: 'Enable %{provider} first'
|
||||
no_service_worker: Service worker not supported
|
||||
specs:
|
||||
tracks: '%{count} tracks'
|
||||
followers: '%{count} followers'
|
||||
@ -13,12 +15,14 @@ specs:
|
||||
edited: 'Edited '
|
||||
actions:
|
||||
play: Play
|
||||
play_all: Play all
|
||||
pause: Pause
|
||||
stop: Stop
|
||||
add_to_library: Add to library
|
||||
remove_from_library: Remove from library
|
||||
reset: Reset
|
||||
back: Back
|
||||
refresh: Refresh
|
||||
enable: Enable
|
||||
disable: Disable
|
||||
send: Send
|
||||
@ -63,6 +67,9 @@ time:
|
||||
services:
|
||||
mopidy:
|
||||
title: Mopidy
|
||||
local: Local
|
||||
spotify:
|
||||
title: Spotify
|
||||
pusher:
|
||||
title: Pusher
|
||||
snapcast:
|
||||
@ -70,6 +77,8 @@ services:
|
||||
genius:
|
||||
title: Genius
|
||||
switch_lyrics_result: Switch to another lyrics seach result
|
||||
google:
|
||||
title: Google
|
||||
lastfm:
|
||||
title: LastFM
|
||||
love: Love
|
||||
@ -245,4 +254,68 @@ settings:
|
||||
user:
|
||||
title: User
|
||||
title_window: '%{name} (user)'
|
||||
you: You
|
||||
you: You
|
||||
discover:
|
||||
categories:
|
||||
title: 'Genre / Mood'
|
||||
category:
|
||||
title: 'Genre / Mood'
|
||||
category: Category
|
||||
featured:
|
||||
title: Featured playlists
|
||||
new_releases:
|
||||
title: New releases
|
||||
recommendations:
|
||||
title: Discover
|
||||
tracks: Tracks
|
||||
artists: Artists
|
||||
albums: Albums
|
||||
body_title: Explore new music
|
||||
body_subtitle: Add seeds and musical properties below to build your sound
|
||||
too_many_seeds: Too many seeds! You can use up to a total of 5 seed tracks, artists and genres.
|
||||
find_recommendations: Find recommendations
|
||||
library:
|
||||
albums:
|
||||
title: My albums
|
||||
artists:
|
||||
title: My artists
|
||||
browse:
|
||||
title: Browse
|
||||
browse_directory:
|
||||
title: Directory
|
||||
playlists:
|
||||
title: My playlists
|
||||
tracks:
|
||||
title: My tracks
|
||||
fields:
|
||||
add_seed_field:
|
||||
placeholder: 'Start typing...'
|
||||
sort: Sort
|
||||
view: View
|
||||
source: Source
|
||||
filters:
|
||||
all: All
|
||||
thumbnails: Thumbnails
|
||||
list: List
|
||||
as_loaded: As loaded
|
||||
name: Name
|
||||
artist: Artist
|
||||
updated: Updated
|
||||
tracks: Tracks
|
||||
source: Source
|
||||
editable: Editable
|
||||
owner: Owner
|
||||
modal:
|
||||
add_to_playlist:
|
||||
title: Add to playlist
|
||||
subtitle: 'Select playlist to add %{count} track%{plural} to'
|
||||
no_playlists: No playlists available
|
||||
no_editable_playlists: No editable playlists
|
||||
add_to_queue:
|
||||
title: Add to queue
|
||||
subtitle: Add a comma-separated list of URIs to the play queue. You must have the appropriate Mopidy backend enabled for each URI schema (eg spotify:, yt:).
|
||||
uris: URI(s)
|
||||
position:
|
||||
label: Position
|
||||
end: End
|
||||
next: After current track
|
||||
|
||||
@ -6,12 +6,13 @@ import { uriSource } from './helpers';
|
||||
* @param object Object
|
||||
* @return Array
|
||||
*/
|
||||
const indexToArray = (index) => {
|
||||
const array = [];
|
||||
for (let key in index) {
|
||||
if (index.hasOwnProperty(key)) array.push(index[key]);
|
||||
const indexToArray = (index, keys) => {
|
||||
if (!index) return [];
|
||||
|
||||
if (keys) {
|
||||
return keys.map((key) => index[key]);
|
||||
}
|
||||
return array;
|
||||
return Object.keys(index).map((key) => index[key]);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -65,6 +65,8 @@ class Settings extends React.Component {
|
||||
}
|
||||
|
||||
resetServiceWorkerAndCache = () => {
|
||||
const { coreActions: { handleException } } = this.props;
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
|
||||
// Hose out all our caches
|
||||
@ -87,24 +89,7 @@ class Settings extends React.Component {
|
||||
window.location = '#';
|
||||
window.location.reload(true);
|
||||
} else {
|
||||
this.props.coreActions.handleException('Service Worker not supported');
|
||||
}
|
||||
}
|
||||
|
||||
handleBlur(service, name, value) {
|
||||
this.setState({ input_in_focus: null });
|
||||
const data = {};
|
||||
data[name] = value;
|
||||
this.props[`${service}Actions`].set(data);
|
||||
|
||||
// Any per-field actions
|
||||
switch (name) {
|
||||
case 'library_albums_uri':
|
||||
this.props.mopidyActions.clearLibraryAlbums();
|
||||
break;
|
||||
case 'library_artists_uri':
|
||||
this.props.mopidyActions.clearLibraryArtists();
|
||||
break;
|
||||
handleException(i18n('errors.no_service_worker'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -11,43 +11,53 @@ import * as spotifyActions from '../../services/spotify/actions';
|
||||
import {
|
||||
isLoading,
|
||||
} from '../../util/helpers';
|
||||
import { i18n, I18n } from '../../locale';
|
||||
import { indexToArray } from '../../util/arrays';
|
||||
|
||||
class DiscoverCategories extends React.Component {
|
||||
componentDidMount() {
|
||||
const {
|
||||
categories,
|
||||
spotifyActions: {
|
||||
getCategories,
|
||||
},
|
||||
uiActions: {
|
||||
setWindowTitle,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
// Check for an empty category index, or where we've only got one loaded
|
||||
// This would be the case if you've refreshed from within a category and only loaded
|
||||
// the single record.
|
||||
if (!this.props.categories || Object.keys(this.props.categories).length <= 1) {
|
||||
this.props.spotifyActions.getCategories();
|
||||
if (!categories || Object.keys(categories).length <= 1) {
|
||||
getCategories();
|
||||
}
|
||||
this.props.uiActions.setWindowTitle('Genre / Mood');
|
||||
setWindowTitle(i18n('discover.categories.title'));
|
||||
}
|
||||
|
||||
render() {
|
||||
if (isLoading(this.props.load_queue, ['spotify_browse/categories'])) {
|
||||
render = () => {
|
||||
const {
|
||||
load_queue,
|
||||
categories: categoriesProp,
|
||||
uiActions,
|
||||
} = this.props;
|
||||
|
||||
if (isLoading(load_queue, ['spotify_browse/categories'])) {
|
||||
return (
|
||||
<div className="view discover-categories-view">
|
||||
<Header icon="grid" title="Genre / Mood" />
|
||||
<Header icon="grid" title={i18n('discover.categories.title')} />
|
||||
<Loader body loading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// convert categories object into simple array
|
||||
const categories = [];
|
||||
if (this.props.categories) {
|
||||
for (const key in this.props.categories) {
|
||||
if (this.props.categories.hasOwnProperty(key)) {
|
||||
categories.push(this.props.categories[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
const categories = indexToArray(categoriesProp);
|
||||
|
||||
return (
|
||||
<div className="view discover-categories-view">
|
||||
<Header uiActions={this.props.uiActions}>
|
||||
<Header uiActions={uiActions}>
|
||||
<Icon name="mood" type="material" />
|
||||
Genre / Mood
|
||||
<I18n path="discover.categories.title" />
|
||||
</Header>
|
||||
<section className="content-wrapper grid-wrapper">
|
||||
<CategoryGrid categories={categories} />
|
||||
@ -57,14 +67,7 @@ class DiscoverCategories extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export our component
|
||||
*
|
||||
* We also integrate our global store, using connect()
|
||||
* */
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
const mapStateToProps = (state) => ({
|
||||
categories: state.spotify.categories,
|
||||
load_queue: state.ui.load_queue,
|
||||
});
|
||||
|
||||
@ -11,6 +11,7 @@ import * as uiActions from '../../services/ui/actions';
|
||||
import * as spotifyActions from '../../services/spotify/actions';
|
||||
import { isLoading } from '../../util/helpers';
|
||||
import { collate } from '../../util/format';
|
||||
import { i18n } from '../../locale';
|
||||
|
||||
class DiscoverCategory extends React.Component {
|
||||
componentDidMount() {
|
||||
@ -39,57 +40,93 @@ class DiscoverCategory extends React.Component {
|
||||
if (!prevCategory && category) this.setWindowTitle(category);
|
||||
}
|
||||
|
||||
setWindowTitle(category = this.props.category) {
|
||||
setWindowTitle = (category = this.props.category) => {
|
||||
const {
|
||||
uiActions: { setWindowTitle },
|
||||
} = this.props;
|
||||
|
||||
if (category) {
|
||||
this.props.uiActions.setWindowTitle(category.name);
|
||||
setWindowTitle(category.name);
|
||||
} else {
|
||||
this.props.uiActions.setWindowTitle('Genre / Mood');
|
||||
setWindowTitle(i18n('discover.category.title'));
|
||||
}
|
||||
}
|
||||
|
||||
loadCategory() {
|
||||
if (!this.props.category) {
|
||||
this.props.spotifyActions.getCategory(this.props.match.params.id);
|
||||
loadCategory = () => {
|
||||
const {
|
||||
category,
|
||||
match: {
|
||||
params: { id },
|
||||
},
|
||||
spotifyActions: {
|
||||
getCategory,
|
||||
getCategoryPlaylists,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
if (!category) {
|
||||
getCategory(id);
|
||||
}
|
||||
|
||||
if (!this.props.category.playlists_uris) {
|
||||
this.props.spotifyActions.getCategoryPlaylists(this.props.match.params.id);
|
||||
if (!category.playlists_uris) {
|
||||
getCategoryPlaylists(id);
|
||||
}
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
this.props.spotifyActions.getMore(
|
||||
this.props.category.playlists_more,
|
||||
loadMore = () => {
|
||||
const {
|
||||
spotifyActions: {
|
||||
getMore,
|
||||
},
|
||||
category: {
|
||||
playlists_more,
|
||||
},
|
||||
match: {
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
getMore(
|
||||
playlists_more,
|
||||
null,
|
||||
{
|
||||
type: 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED_MORE',
|
||||
uri: `category:${this.props.match.params.id}`,
|
||||
uri: `category:${id}`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (isLoading(this.props.load_queue, ['spotify_browse/categories/'])) {
|
||||
render = () => {
|
||||
const {
|
||||
category: categoryProp,
|
||||
playlists,
|
||||
load_queue,
|
||||
uiActions,
|
||||
} = this.props;
|
||||
|
||||
if (isLoading(load_queue, ['spotify_browse/categories/'])) {
|
||||
return (
|
||||
<div className="view discover-categories-view">
|
||||
<Header>
|
||||
<Icon name="mood" type="material" />
|
||||
{(this.props.category ? this.props.category.name : 'Category')}
|
||||
{(categoryProp ? categoryProp.name : i18n('discover.category.category'))}
|
||||
</Header>
|
||||
<Loader body loading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.props.category) {
|
||||
if (!categoryProp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const category = collate(this.props.category, { playlists: this.props.playlists });
|
||||
const category = collate(categoryProp, { playlists });
|
||||
|
||||
return (
|
||||
<div className="view discover-categories-view">
|
||||
<Header uiActions={this.props.uiActions}>
|
||||
<Header uiActions={uiActions}>
|
||||
<Icon name="mood" type="material" />
|
||||
{category.name}
|
||||
</Header>
|
||||
@ -100,7 +137,7 @@ class DiscoverCategory extends React.Component {
|
||||
<LazyLoadListener
|
||||
loadKey={category.playlists_more}
|
||||
showLoader={category.playlists_more}
|
||||
loadMore={() => this.loadMore()}
|
||||
loadMore={this.loadMore}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -10,6 +10,7 @@ import * as uiActions from '../../services/ui/actions';
|
||||
import * as mopidyActions from '../../services/mopidy/actions';
|
||||
import * as spotifyActions from '../../services/spotify/actions';
|
||||
import { isLoading } from '../../util/helpers';
|
||||
import { i18n, I18n } from '../../locale';
|
||||
|
||||
class DiscoverFeatured extends React.Component {
|
||||
componentDidMount() {
|
||||
@ -23,7 +24,7 @@ class DiscoverFeatured extends React.Component {
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
setWindowTitle('Featured playlists');
|
||||
setWindowTitle(i18n('discover.featured.title'));
|
||||
if (!featured_playlists) getFeaturedPlaylists();
|
||||
}
|
||||
|
||||
@ -40,7 +41,7 @@ class DiscoverFeatured extends React.Component {
|
||||
getFeaturedPlaylists();
|
||||
}
|
||||
|
||||
handleContextMenu(e, item) {
|
||||
handleContextMenu = (e, item) => {
|
||||
const {
|
||||
uiActions: {
|
||||
showContextMenu,
|
||||
@ -70,7 +71,7 @@ class DiscoverFeatured extends React.Component {
|
||||
<div className="view discover-featured-view preserve-3d">
|
||||
<Header className="overlay" uiActions={uiActions}>
|
||||
<Icon name="star" type="material" />
|
||||
Featured playlists
|
||||
<I18n path="discover.featured.title" />
|
||||
</Header>
|
||||
<Loader body loading />
|
||||
</div>
|
||||
@ -90,7 +91,7 @@ class DiscoverFeatured extends React.Component {
|
||||
const options = (
|
||||
<a className="button button--no-hover" onClick={this.onRefresh}>
|
||||
<Icon name="refresh" />
|
||||
Refresh
|
||||
<I18n path="actions.refresh" />
|
||||
</a>
|
||||
);
|
||||
|
||||
@ -98,7 +99,7 @@ class DiscoverFeatured extends React.Component {
|
||||
<div className="view discover-featured-view preserve-3d">
|
||||
<Header uiActions={uiActions} options={options}>
|
||||
<Icon name="star" type="material" />
|
||||
Featured playlists
|
||||
<I18n path="discover.featured.title" />
|
||||
</Header>
|
||||
<section className="content-wrapper grid-wrapper">
|
||||
{items && <PlaylistGrid playlists={items} />}
|
||||
|
||||
@ -12,20 +12,37 @@ import * as uiActions from '../../services/ui/actions';
|
||||
import * as mopidyActions from '../../services/mopidy/actions';
|
||||
import * as spotifyActions from '../../services/spotify/actions';
|
||||
import { isLoading } from '../../util/helpers';
|
||||
import { collate } from '../../util/format';
|
||||
import { i18n, I18n } from '../../locale';
|
||||
|
||||
class DiscoverNewReleases extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.uiActions.setWindowTitle('New releases');
|
||||
const {
|
||||
new_releases,
|
||||
uiActions: {
|
||||
setWindowTitle,
|
||||
},
|
||||
spotifyActions: {
|
||||
getNewReleases,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
if (!this.props.new_releases) {
|
||||
this.props.spotifyActions.getNewReleases();
|
||||
setWindowTitle(i18n('discover.new_releases.title'));
|
||||
|
||||
if (!new_releases) {
|
||||
getNewReleases();
|
||||
}
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
this.props.spotifyActions.getMore(
|
||||
this.props.new_releases_more,
|
||||
loadMore = () => {
|
||||
const {
|
||||
new_releases_more,
|
||||
spotifyActions: {
|
||||
getMore,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
getMore(
|
||||
new_releases_more,
|
||||
null,
|
||||
{
|
||||
type: 'SPOTIFY_NEW_RELEASES_LOADED',
|
||||
@ -33,19 +50,36 @@ class DiscoverNewReleases extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
playAlbum(e, album) {
|
||||
this.props.mopidyActions.playURIs([album.uri], album.uri);
|
||||
playAlbum = (album) => {
|
||||
const { mopidyActions: { playURIs } } = this.props;
|
||||
|
||||
playURIs([album.uri], album.uri);
|
||||
}
|
||||
|
||||
refresh = () => {
|
||||
const {
|
||||
uiActions: {
|
||||
hideContextMenu,
|
||||
},
|
||||
spotifyActions: {
|
||||
getNewReleases,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
hideContextMenu();
|
||||
getNewReleases();
|
||||
}
|
||||
|
||||
handleContextMenu(e, item) {
|
||||
const { uriActions: { showContextMenu } } = this.props;
|
||||
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
showContextMenu({
|
||||
e,
|
||||
context: 'album',
|
||||
uris: [item.uri],
|
||||
items: [item],
|
||||
};
|
||||
this.props.uiActions.showContextMenu(data);
|
||||
});
|
||||
}
|
||||
|
||||
renderIntro = ({ images: { large } = {} } = {}) => (
|
||||
@ -54,13 +88,21 @@ class DiscoverNewReleases extends React.Component {
|
||||
</div>
|
||||
);
|
||||
|
||||
render() {
|
||||
if (isLoading(this.props.load_queue, ['spotify_browse/new-releases'])) {
|
||||
render = () => {
|
||||
const {
|
||||
load_queue,
|
||||
new_releases,
|
||||
albums: albumsProp,
|
||||
new_releases_more,
|
||||
uiActions,
|
||||
} = this.props;
|
||||
|
||||
if (isLoading(load_queue, ['spotify_browse/new-releases'])) {
|
||||
return (
|
||||
<div className="view discover-new-releases-view">
|
||||
<Header>
|
||||
<Icon name="new_releases" type="material" />
|
||||
New releases
|
||||
<I18n path="discover.new_releases.title" />
|
||||
</Header>
|
||||
<Loader body loading />
|
||||
</div>
|
||||
@ -68,41 +110,41 @@ class DiscoverNewReleases extends React.Component {
|
||||
}
|
||||
|
||||
const albums = [];
|
||||
if (this.props.new_releases) {
|
||||
for (const uri of this.props.new_releases) {
|
||||
if (this.props.albums.hasOwnProperty(uri)) {
|
||||
albums.push(this.props.albums[uri]);
|
||||
if (new_releases) {
|
||||
for (const uri of new_releases) {
|
||||
if (albumsProp.hasOwnProperty(uri)) {
|
||||
albums.push(albumsProp[uri]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const options = (
|
||||
<a className="button button--no-hover" onClick={(e) => { this.props.uiActions.hideContextMenu(); this.props.spotifyActions.getNewReleases(); }}>
|
||||
<a className="button button--no-hover" onClick={this.refresh}>
|
||||
<Icon name="refresh" />
|
||||
Refresh
|
||||
<I18n path="actions.refresh" />
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="view discover-new-releases-view preserve-3d">
|
||||
<Header options={options} uiActions={this.props.uiActions}>
|
||||
<Header options={options} uiActions={uiActions}>
|
||||
<Icon name="new_releases" type="material" />
|
||||
New releases
|
||||
<I18n path="discover.new_releases.title" />
|
||||
</Header>
|
||||
<section className="content-wrapper grid-wrapper">
|
||||
<AlbumGrid albums={albums} />
|
||||
</section>
|
||||
<LazyLoadListener
|
||||
loadKey={this.props.new_releases_more}
|
||||
showLoader={this.props.new_releases_more}
|
||||
loadMore={() => this.loadMore()}
|
||||
loadKey={new_releases_more}
|
||||
showLoader={new_releases_more}
|
||||
loadMore={this.loadMore}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
const mapStateToProps = (state) => ({
|
||||
theme: state.ui.theme,
|
||||
load_queue: state.ui.load_queue,
|
||||
artists: state.core.artists,
|
||||
|
||||
@ -22,7 +22,8 @@ import {
|
||||
uriType,
|
||||
titleCase,
|
||||
} from '../../util/helpers';
|
||||
import { arrayOf } from '../../util/arrays';
|
||||
import { arrayOf, indexToArray } from '../../util/arrays';
|
||||
import { i18n, I18n } from '../../locale';
|
||||
|
||||
class Discover extends React.Component {
|
||||
constructor(props) {
|
||||
@ -150,15 +151,25 @@ class Discover extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.uiActions.setWindowTitle('Discover');
|
||||
const {
|
||||
uiActions: {
|
||||
setWindowTitle,
|
||||
},
|
||||
match: {
|
||||
params: {
|
||||
seeds,
|
||||
},
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
// We have seeds provided in the URL
|
||||
if (this.props.match.params.seeds) {
|
||||
this.handleURLSeeds(this.props.match.params.seeds);
|
||||
setWindowTitle(i18n('discover.recommendations.title'));
|
||||
|
||||
if (seeds) {
|
||||
this.handleURLSeeds();
|
||||
}
|
||||
}
|
||||
|
||||
componentDiUpdate = ({
|
||||
componentDidUpdate = ({
|
||||
match: {
|
||||
params: {
|
||||
seeds: prevSeeds,
|
||||
@ -172,63 +183,92 @@ class Discover extends React.Component {
|
||||
},
|
||||
},
|
||||
} = this.props;
|
||||
if (prevSeeds !== seeds) this.handleURLSeeds(seeds);
|
||||
if (prevSeeds !== seeds) this.handleURLSeeds();
|
||||
}
|
||||
|
||||
handleContextMenu(e) {
|
||||
var uri = 'iris:discover';
|
||||
if (this.state.seeds) {
|
||||
handleContextMenu = (e) => {
|
||||
const {
|
||||
recommendations: {
|
||||
tracks_uris,
|
||||
},
|
||||
uiActions: {
|
||||
showContextMenu,
|
||||
},
|
||||
tracks: tracksProp,
|
||||
} = this.props;
|
||||
const {
|
||||
seeds,
|
||||
} = this.state;
|
||||
|
||||
let uri = 'iris:discover';
|
||||
if (seeds) {
|
||||
uri += ':';
|
||||
for (var i = 0; i < this.state.seeds.length; i++) {
|
||||
for (let i = 0; i < seeds.length; i++) {
|
||||
if (i > 0) {
|
||||
uri += ',';
|
||||
}
|
||||
uri += this.state.seeds[i].split(':').join('_');
|
||||
uri += seeds[i].split(':').join('_');
|
||||
}
|
||||
}
|
||||
|
||||
const tracks = [];
|
||||
if (this.props.recommendations.tracks_uris && this.props.tracks) {
|
||||
for (var i = 0; i < this.props.recommendations.tracks_uris.length; i++) {
|
||||
var uri = this.props.recommendations.tracks_uris[i];
|
||||
if (this.props.tracks.hasOwnProperty(uri)) {
|
||||
tracks.push(this.props.tracks[uri]);
|
||||
}
|
||||
}
|
||||
}
|
||||
const tracks = indexToArray(tracksProp, tracks_uris);
|
||||
|
||||
const data = {
|
||||
showContextMenu({
|
||||
e,
|
||||
context: 'track',
|
||||
items: tracks,
|
||||
uris: arrayOf('uri', tracks),
|
||||
};
|
||||
this.props.uiActions.showContextMenu(data);
|
||||
});
|
||||
}
|
||||
|
||||
handleURLSeeds(seeds_string = this.props.match.params.seeds) {
|
||||
handleURLSeeds = () => {
|
||||
const {
|
||||
spotifyActions: {
|
||||
getArtist,
|
||||
getTrack,
|
||||
},
|
||||
match: {
|
||||
params: {
|
||||
seeds: seedsProp,
|
||||
},
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
// Rejoin if we've had to uri-encode these as strings
|
||||
// We'd need to do this if our URL has been encoded so the whole URL can become
|
||||
// it's own URI (eg iris:discover:spotify_artist_1234) where we can't use ":"
|
||||
const seeds = seeds_string.split('_').join(':').split(',');
|
||||
const seeds = seedsProp.split('_').join(':').split(',');
|
||||
|
||||
for (let i = 0; i < seeds.length; i++) {
|
||||
switch (uriType(seeds[i])) {
|
||||
case 'artist':
|
||||
this.props.spotifyActions.getArtist(seeds[i]);
|
||||
getArtist(seeds[i]);
|
||||
break;
|
||||
|
||||
case 'track':
|
||||
this.props.spotifyActions.getTrack(seeds[i]);
|
||||
getTrack(seeds[i]);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ seeds });
|
||||
this.getRecommendations(seeds);
|
||||
this.setState(
|
||||
{ seeds },
|
||||
() => this.getRecommendations(),
|
||||
);
|
||||
}
|
||||
|
||||
getRecommendations(seeds = this.state.seeds, tunabilities = this.state.tunabilities) {
|
||||
getRecommendations = () => {
|
||||
const {
|
||||
seeds,
|
||||
tunabilities,
|
||||
} = this.state;
|
||||
const {
|
||||
spotifyActions: {
|
||||
getRecommendations: doGetRecommendations,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
if (seeds.length > 0) {
|
||||
const digested_tunabilities = {};
|
||||
for (const key in tunabilities) {
|
||||
@ -247,37 +287,53 @@ class Discover extends React.Component {
|
||||
digested_tunabilities[`${key}_min`] = min.toString();
|
||||
}
|
||||
}
|
||||
this.props.spotifyActions.getRecommendations(seeds, 50, digested_tunabilities);
|
||||
doGetRecommendations(seeds, 50, digested_tunabilities);
|
||||
}
|
||||
}
|
||||
|
||||
playTracks() {
|
||||
this.props.mopidyActions.playURIs(this.props.recommendations.tracks_uris);
|
||||
playTracks = () => {
|
||||
const {
|
||||
mopidyActions: {
|
||||
playURIs,
|
||||
},
|
||||
recommendations: {
|
||||
tracks_uris,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
playURIs(tracks_uris);
|
||||
}
|
||||
|
||||
removeSeed(index) {
|
||||
removeSeed = (index) => {
|
||||
const { seeds } = this.state;
|
||||
seeds.splice(index, 1);
|
||||
this.setState({ seeds });
|
||||
}
|
||||
|
||||
handleSelect(e, uri) {
|
||||
handleSelect = (e, uri) => {
|
||||
const { seeds } = this.state;
|
||||
seeds.push(uri);
|
||||
this.setState({ seeds });
|
||||
}
|
||||
|
||||
renderSeeds() {
|
||||
renderSeeds = () => {
|
||||
const {
|
||||
tracks,
|
||||
artists,
|
||||
} = this.props;
|
||||
const {
|
||||
seeds,
|
||||
} = this.state;
|
||||
const seeds_objects = [];
|
||||
|
||||
if (this.state.seeds.length > 0) {
|
||||
for (let i = 0; i < this.state.seeds.length; i++) {
|
||||
const uri = this.state.seeds[i];
|
||||
if (seeds.length > 0) {
|
||||
for (let i = 0; i < seeds.length; i++) {
|
||||
const uri = seeds[i];
|
||||
|
||||
switch (uriType(uri)) {
|
||||
case 'track':
|
||||
if (typeof (this.props.tracks[uri]) !== 'undefined') {
|
||||
seeds_objects.push(this.props.tracks[uri]);
|
||||
if (typeof (tracks[uri]) !== 'undefined') {
|
||||
seeds_objects.push(tracks[uri]);
|
||||
} else {
|
||||
seeds_objects.push({
|
||||
name: 'Loading...',
|
||||
@ -285,10 +341,9 @@ class Discover extends React.Component {
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'artist':
|
||||
if (typeof (this.props.artists[uri]) !== 'undefined') {
|
||||
seeds_objects.push(this.props.artists[uri]);
|
||||
if (typeof (artists[uri]) !== 'undefined') {
|
||||
seeds_objects.push(artists[uri]);
|
||||
} else {
|
||||
seeds_objects.push({
|
||||
name: 'Loading...',
|
||||
@ -296,7 +351,6 @@ class Discover extends React.Component {
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'genre':
|
||||
var name = getFromUri('genreid', uri);
|
||||
seeds_objects.push({
|
||||
@ -304,6 +358,8 @@ class Discover extends React.Component {
|
||||
uri,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -311,22 +367,26 @@ class Discover extends React.Component {
|
||||
return (
|
||||
<div className="seeds">
|
||||
{
|
||||
seeds_objects.map((seed, index) => {
|
||||
const type = uriType(seed.uri);
|
||||
let images = null;
|
||||
if (seed.images) {
|
||||
if (type == 'artist') {
|
||||
if (seed.images.length > 0) {
|
||||
images = seed.images[0];
|
||||
}
|
||||
} else {
|
||||
images = seed.images;
|
||||
}
|
||||
}
|
||||
seeds_objects.map((seed, index) => {
|
||||
const type = uriType(seed.uri);
|
||||
let images = null;
|
||||
if (seed.images) {
|
||||
if (type === 'artist') {
|
||||
if (seed.images.length > 0) {
|
||||
images = seed.images[0];
|
||||
}
|
||||
} else {
|
||||
images = seed.images;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`seed${seed.images ? ' has-thumbnail' : ''}`} key={seed.uri}>
|
||||
{images ? <URILink className="thumbnail-wrapper" type={type} uri={seed.uri}><Thumbnail images={images} circle={seed.type == 'artist'} size="small" /></URILink> : null}
|
||||
{images && (
|
||||
<URILink className="thumbnail-wrapper" type={type} uri={seed.uri}>
|
||||
<Thumbnail images={images} circle={seed.type === 'artist'} size="small" />
|
||||
</URILink>
|
||||
)}
|
||||
<div className="seed__details">
|
||||
<div className="seed__label">
|
||||
<span className="seed__label__text">{titleCase(type)}</span>
|
||||
@ -342,19 +402,19 @@ class Discover extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
setTunability(name, value) {
|
||||
setTunability = (name, value) => {
|
||||
const { tunabilities } = this.state;
|
||||
tunabilities[name].value = value;
|
||||
this.setState({ tunabilities });
|
||||
}
|
||||
|
||||
toggleTunability(name) {
|
||||
toggleTunability = (name) => {
|
||||
const { tunabilities } = this.state;
|
||||
tunabilities[name].enabled = !tunabilities[name].enabled;
|
||||
this.setState({ tunabilities });
|
||||
}
|
||||
|
||||
renderTunabilities() {
|
||||
renderTunabilities = () => {
|
||||
const addable_tunabilities = [];
|
||||
const enabled_tunabilities = [];
|
||||
for (const key in this.state.tunabilities) {
|
||||
@ -382,7 +442,7 @@ class Discover extends React.Component {
|
||||
<div className="field tunability range" key={tunability.name}>
|
||||
<div className="tunability__label">
|
||||
{titleCase(tunability.name)}
|
||||
<span className="remove" onClick={(e) => this.toggleTunability(tunability.name)}>
|
||||
<span className="remove" onClick={() => this.toggleTunability(tunability.name)}>
|
||||
<Icon name="close" />
|
||||
</span>
|
||||
</div>
|
||||
@ -401,38 +461,51 @@ class Discover extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderResults() {
|
||||
// Results not in
|
||||
if (!this.props.recommendations || this.props.recommendations.albums_uris === undefined || this.props.recommendations.artists_uris === undefined) {
|
||||
renderResults = () => {
|
||||
const {
|
||||
recommendations: {
|
||||
tracks_uris,
|
||||
albums_uris,
|
||||
artists_uris,
|
||||
} = {},
|
||||
tracks: tracksProp,
|
||||
artists: artistsProp,
|
||||
albums: albumsProp,
|
||||
} = this.props;
|
||||
const {
|
||||
seeds,
|
||||
} = this.state;
|
||||
|
||||
if (!albums_uris === undefined || artists_uris === undefined) {
|
||||
return <div className="content-wrapper recommendations-results" />;
|
||||
}
|
||||
|
||||
const tracks = [];
|
||||
if (this.props.recommendations.tracks_uris && this.props.tracks) {
|
||||
for (var i = 0; i < this.props.recommendations.tracks_uris.length; i++) {
|
||||
var uri = this.props.recommendations.tracks_uris[i];
|
||||
if (this.props.tracks.hasOwnProperty(uri)) {
|
||||
tracks.push(this.props.tracks[uri]);
|
||||
if (tracks_uris && tracksProp) {
|
||||
for (var i = 0; i < tracks_uris.length; i++) {
|
||||
var uri = tracks_uris[i];
|
||||
if (tracksProp.hasOwnProperty(uri)) {
|
||||
tracks.push(tracksProp[uri]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const artists = [];
|
||||
if (this.props.recommendations.artists_uris && this.props.artists) {
|
||||
for (var i = 0; i < this.props.recommendations.artists_uris.length; i++) {
|
||||
var uri = this.props.recommendations.artists_uris[i];
|
||||
if (this.props.artists.hasOwnProperty(uri)) {
|
||||
artists.push(this.props.artists[uri]);
|
||||
if (artists_uris && artistsProp) {
|
||||
for (var i = 0; i < artists_uris.length; i++) {
|
||||
var uri = artists_uris[i];
|
||||
if (artistsProp.hasOwnProperty(uri)) {
|
||||
artists.push(artistsProp[uri]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const albums = [];
|
||||
if (this.props.recommendations.albums_uris && this.props.albums) {
|
||||
for (var i = 0; i < this.props.recommendations.albums_uris.length; i++) {
|
||||
var uri = this.props.recommendations.albums_uris[i];
|
||||
if (this.props.albums.hasOwnProperty(uri)) {
|
||||
albums.push(this.props.albums[uri]);
|
||||
if (albums_uris && albumsProp) {
|
||||
for (var i = 0; i < albums_uris.length; i++) {
|
||||
var uri = albums_uris[i];
|
||||
if (albumsProp.hasOwnProperty(uri)) {
|
||||
albums.push(albumsProp[uri]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -443,13 +516,13 @@ class Discover extends React.Component {
|
||||
}
|
||||
|
||||
var uri = 'iris:discover';
|
||||
if (this.state.seeds) {
|
||||
if (seeds) {
|
||||
uri += ':';
|
||||
for (var i = 0; i < this.state.seeds.length; i++) {
|
||||
for (var i = 0; i < seeds.length; i++) {
|
||||
if (i > 0) {
|
||||
uri += ',';
|
||||
}
|
||||
uri += this.state.seeds[i].split(':').join('_');
|
||||
uri += seeds[i].split(':').join('_');
|
||||
}
|
||||
}
|
||||
|
||||
@ -458,10 +531,12 @@ class Discover extends React.Component {
|
||||
|
||||
<section className="col col--w70 tracks">
|
||||
<h4>
|
||||
Tracks
|
||||
<I18n path="discover.recommendations.tracks" />
|
||||
<div className="pull-right">
|
||||
<ContextMenuTrigger onTrigger={(e) => this.handleContextMenu(e)} />
|
||||
<button className="button button--primary" onClick={(e) => this.playTracks(e)}>Play all</button>
|
||||
<ContextMenuTrigger onTrigger={this.handleContextMenu} />
|
||||
<button className="button button--primary" onClick={this.playTracks} type="button">
|
||||
<I18n path="actions.play_all" />
|
||||
</button>
|
||||
</div>
|
||||
</h4>
|
||||
<TrackList className="discover-track-list" uri={uri} tracks={tracks} />
|
||||
@ -471,13 +546,13 @@ class Discover extends React.Component {
|
||||
|
||||
<div className="col col--w25 others">
|
||||
<section>
|
||||
<h4>Artists</h4>
|
||||
<RelatedArtists artists={artists} uiActions={this.props.uiActions} />
|
||||
<h4><I18n path="discover.recommendations.artists" /></h4>
|
||||
<RelatedArtists artists={artists} uiActions={uiActions} />
|
||||
</section>
|
||||
<br />
|
||||
<br />
|
||||
<section>
|
||||
<h4>Albums</h4>
|
||||
<h4><I18n path="discover.recommendations.albums" /></h4>
|
||||
<AlbumGrid className="grid--mini" albums={albums} />
|
||||
</section>
|
||||
</div>
|
||||
@ -486,14 +561,22 @@ class Discover extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const is_loading = isLoading(this.props.load_queue, ['spotify_recommendations']);
|
||||
render = () => {
|
||||
const {
|
||||
load_queue,
|
||||
theme,
|
||||
} = this.props;
|
||||
const {
|
||||
tunabilities,
|
||||
seeds,
|
||||
} = this.state;
|
||||
|
||||
const is_loading = isLoading(load_queue, ['spotify_recommendations']);
|
||||
const addable_tunabilities = [];
|
||||
for (const key in this.state.tunabilities) {
|
||||
if (this.state.tunabilities.hasOwnProperty(key)) {
|
||||
if (tunabilities.hasOwnProperty(key)) {
|
||||
const tunability = {
|
||||
|
||||
...this.state.tunabilities[key],
|
||||
...tunabilities[key],
|
||||
name: key,
|
||||
};
|
||||
|
||||
@ -509,30 +592,39 @@ class Discover extends React.Component {
|
||||
return (
|
||||
<div className="view discover-view preserve-3d">
|
||||
<div className="intro preserve-3d">
|
||||
|
||||
{this.props.theme == 'dark' && <Parallax image="/iris/assets/backgrounds/discover.jpg" />}
|
||||
|
||||
{theme === 'dark' && <Parallax image="/iris/assets/backgrounds/discover.jpg" />}
|
||||
<div className="intro__liner">
|
||||
<h1>Explore new music</h1>
|
||||
<h2>Add seeds and musical properties below to build your sound</h2>
|
||||
<h1><I18n path="discover.recommendations.body_title" /></h1>
|
||||
<h2><I18n path="discover.recommendations.body_subtitle" /></h2>
|
||||
<div className="intro__parameters">
|
||||
|
||||
{this.renderSeeds()}
|
||||
{this.renderTunabilities()}
|
||||
{this.state.seeds.length > 5 ? <p className="message error">Too many seeds! You can use up to a total of 5 seed tracks, artists and genres.</p> : null}
|
||||
|
||||
{seeds.length > 5 && (
|
||||
<p className="message error">
|
||||
<I18n path="discover.recommendations.too_many_seeds" />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="intro__actions">
|
||||
|
||||
<AddSeedField onSelect={(e, uri) => this.handleSelect(e, uri)} />
|
||||
<DropdownField className="add-properties" name="Properties" options={addable_tunabilities} no_status_icon button="default" handleChange={(val) => { this.toggleTunability(val); }} />
|
||||
<DropdownField
|
||||
className="add-properties"
|
||||
name="Properties"
|
||||
options={addable_tunabilities}
|
||||
no_status_icon
|
||||
button="default"
|
||||
handleChange={this.toggleTunability}
|
||||
/>
|
||||
<div className="intro__actions__separator" />
|
||||
<span className={`submit button button--primary button--large${is_loading ? ' button--working' : ''}`} onClick={(e) => this.getRecommendations()}>
|
||||
<span
|
||||
className={`submit button button--primary button--large${is_loading ? ' button--working' : ''}`}
|
||||
onClick={this.getRecommendations}
|
||||
>
|
||||
<Icon name="explore" />
|
||||
|
||||
Find recommendations
|
||||
<I18n path="discover.recommendations.find_recommendations" afterContent>
|
||||
{' '}
|
||||
</I18n>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -545,7 +637,7 @@ class Discover extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
const mapStateToProps = (state) => ({
|
||||
theme: state.ui.theme,
|
||||
albums: state.core.albums,
|
||||
artists: state.core.artists,
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
} from '../../util/helpers';
|
||||
import { sortItems, applyFilter } from '../../util/arrays';
|
||||
import { collate } from '../../util/format';
|
||||
import { i18n, I18n } from '../../locale';
|
||||
|
||||
class LibraryAlbums extends React.Component {
|
||||
constructor(props) {
|
||||
@ -32,26 +33,44 @@ class LibraryAlbums extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
location: {
|
||||
state = {},
|
||||
},
|
||||
uiActions: {
|
||||
setWindowTitle,
|
||||
},
|
||||
mopidy_connected,
|
||||
mopidy_library_albums_status,
|
||||
source,
|
||||
mopidyActions,
|
||||
spotifyActions,
|
||||
googleActions,
|
||||
google_available,
|
||||
google_library_albums_status,
|
||||
spotify_available,
|
||||
spotify_library_albums_status,
|
||||
} = this.props;
|
||||
|
||||
// Restore any limit defined in our location state
|
||||
const state = (this.props.location.state ? this.props.location.state : {});
|
||||
if (state.limit) {
|
||||
this.setState({
|
||||
limit: state.limit,
|
||||
});
|
||||
}
|
||||
|
||||
this.props.uiActions.setWindowTitle('Albums');
|
||||
setWindowTitle(i18n('library.albums.title'));
|
||||
|
||||
if (this.props.mopidy_connected && this.props.mopidy_library_albums_status != 'finished' && this.props.mopidy_library_albums_status != 'started' && (this.props.source == 'all' || this.props.source == 'local')) {
|
||||
this.props.mopidyActions.getLibraryAlbums();
|
||||
if (mopidy_connected && mopidy_library_albums_status !== 'finished' && mopidy_library_albums_status !== 'started' && (source === 'all' || source === 'local')) {
|
||||
mopidyActions.getLibraryAlbums();
|
||||
}
|
||||
|
||||
if (this.props.google_available && this.props.google_library_albums_status != 'finished' && this.props.google_library_albums_status != 'started' && (this.props.source == 'all' || this.props.source == 'google')) {
|
||||
this.props.googleActions.getLibraryAlbums();
|
||||
if (google_available && google_library_albums_status !== 'finished' && google_library_albums_status !== 'started' && (source === 'all' || source === 'google')) {
|
||||
googleActions.getLibraryAlbums();
|
||||
}
|
||||
|
||||
if (this.props.spotify_available && this.props.spotify_library_albums_status != 'finished' && this.props.spotify_library_albums_status != 'started' && (this.props.source == 'all' || this.props.source == 'spotify')) {
|
||||
this.props.spotifyActions.getLibraryAlbums();
|
||||
if (spotify_available && spotify_library_albums_status !== 'finished' && spotify_library_albums_status !== 'started' && (source == 'all' || source === 'spotify')) {
|
||||
spotifyActions.getLibraryAlbums();
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,7 +90,7 @@ class LibraryAlbums extends React.Component {
|
||||
spotify_library_albums_status,
|
||||
} = this.props;
|
||||
|
||||
if (mopidy_connected && (source == 'all' || source == 'local')) {
|
||||
if (mopidy_connected && (source === 'all' || source === 'local')) {
|
||||
if (!prev_mopidy_connected) mopidyActions.getLibraryAlbums();
|
||||
|
||||
// Filter changed, but we haven't got this provider's library yet
|
||||
@ -80,7 +99,7 @@ class LibraryAlbums extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (google_available && (source == 'all' || source == 'google')) {
|
||||
if (google_available && (source === 'all' || source === 'google')) {
|
||||
// Filter changed, but we haven't got this provider's library yet
|
||||
if (source !== 'all' && source !== 'google' && google_library_albums_status !== 'finished' && google_library_albums_status !== 'started') {
|
||||
googleActions.getLibraryAlbums();
|
||||
@ -95,22 +114,32 @@ class LibraryAlbums extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleContextMenu(e, item) {
|
||||
const data = {
|
||||
handleContextMenu = (e, item) => {
|
||||
const {
|
||||
uiActions: {
|
||||
showContextMenu,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
showContextMenu({
|
||||
e,
|
||||
context: 'album',
|
||||
uris: [item.uri],
|
||||
items: [item],
|
||||
};
|
||||
this.props.uiActions.showContextMenu(data);
|
||||
});
|
||||
}
|
||||
|
||||
moreURIsToLoad() {
|
||||
moreURIsToLoad = () => {
|
||||
const {
|
||||
albums,
|
||||
library_albums,
|
||||
} = this.props;
|
||||
|
||||
const uris = [];
|
||||
if (this.props.albums && this.props.library_albums) {
|
||||
for (let i = 0; i < this.props.library_albums.length; i++) {
|
||||
const uri = this.props.library_albums[i];
|
||||
if (!this.props.albums.hasOwnProperty(uri) && uriSource(uri) == 'local') {
|
||||
if (albums && library_albums) {
|
||||
for (let i = 0; i < library_albums.length; i++) {
|
||||
const uri = library_albums[i];
|
||||
if (!albums.hasOwnProperty(uri) && uriSource(uri) == 'local') {
|
||||
uris.push(uri);
|
||||
}
|
||||
|
||||
@ -122,29 +151,42 @@ class LibraryAlbums extends React.Component {
|
||||
return uris;
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
const new_limit = this.state.limit + this.state.per_page;
|
||||
loadMore = () => {
|
||||
const {
|
||||
limit,
|
||||
per_page,
|
||||
} = this.state;
|
||||
const {
|
||||
location: {
|
||||
state,
|
||||
},
|
||||
history,
|
||||
} = this.props;
|
||||
|
||||
const new_limit = limit + per_page;
|
||||
this.setState({ limit: new_limit });
|
||||
|
||||
// Set our pagination to location state
|
||||
const state = (this.props.location && this.props.location.state ? this.props.location.state : {});
|
||||
state.limit = new_limit;
|
||||
this.props.history.replace({ state });
|
||||
history.replace({ state: { ...state, limit: new_limit } });
|
||||
}
|
||||
|
||||
setSort(value) {
|
||||
let reverse = false;
|
||||
if (this.props.sort == value) reverse = !this.props.sort_reverse;
|
||||
setSort = (value) => {
|
||||
const {
|
||||
sort,
|
||||
sort_reverse,
|
||||
uiActions: {
|
||||
set,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
const data = {
|
||||
let reverse = false;
|
||||
if (sort === value) reverse = !sort_reverse;
|
||||
|
||||
set({
|
||||
library_albums_sort_reverse: reverse,
|
||||
library_albums_sort: value,
|
||||
};
|
||||
this.props.uiActions.set(data);
|
||||
});
|
||||
}
|
||||
|
||||
renderView() {
|
||||
renderView = () => {
|
||||
let albums = [];
|
||||
|
||||
// Spotify library items
|
||||
@ -224,7 +266,7 @@ class LibraryAlbums extends React.Component {
|
||||
<LazyLoadListener
|
||||
loadKey={total_albums > this.state.limit ? this.state.limit : total_albums}
|
||||
showLoader={this.state.limit < total_albums}
|
||||
loadMore={() => this.loadMore()}
|
||||
loadMore={this.loadMore}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@ -248,63 +290,63 @@ class LibraryAlbums extends React.Component {
|
||||
const source_options = [
|
||||
{
|
||||
value: 'all',
|
||||
label: 'All',
|
||||
label: i18n('fields.filters.all'),
|
||||
},
|
||||
{
|
||||
value: 'local',
|
||||
label: 'Local',
|
||||
label: i18n('services.spotify.local'),
|
||||
},
|
||||
];
|
||||
|
||||
if (this.props.spotify_available) {
|
||||
source_options.push({
|
||||
value: 'spotify',
|
||||
label: 'Spotify',
|
||||
label: i18n('services.spotify.title'),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.google_available) {
|
||||
source_options.push({
|
||||
value: 'google',
|
||||
label: 'Google',
|
||||
label: i18n('services.google.title'),
|
||||
});
|
||||
}
|
||||
|
||||
const view_options = [
|
||||
{
|
||||
value: 'thumbnails',
|
||||
label: 'Thumbnails',
|
||||
label: i18n('fields.filters.thumbnails'),
|
||||
},
|
||||
{
|
||||
value: 'list',
|
||||
label: 'List',
|
||||
label: i18n('fields.filters.list'),
|
||||
},
|
||||
];
|
||||
|
||||
const sort_options = [
|
||||
{
|
||||
value: null,
|
||||
label: 'As loaded',
|
||||
label: i18n('fields.filters.as_loaded'),
|
||||
},
|
||||
{
|
||||
value: 'name',
|
||||
label: 'Name',
|
||||
label: i18n('fields.filters.name'),
|
||||
},
|
||||
{
|
||||
value: 'artists.first.name',
|
||||
label: 'Artist',
|
||||
label: i18n('fields.filters.artist'),
|
||||
},
|
||||
{
|
||||
value: 'last_modified',
|
||||
label: 'Updated',
|
||||
label: i18n('fields.filters.updated'),
|
||||
},
|
||||
{
|
||||
value: 'tracks_uris.length',
|
||||
label: 'Tracks',
|
||||
label: i18n('fields.filters.tracks'),
|
||||
},
|
||||
{
|
||||
value: 'uri',
|
||||
label: 'Source',
|
||||
label: i18n('fields.filters.source'),
|
||||
},
|
||||
];
|
||||
|
||||
@ -317,7 +359,7 @@ class LibraryAlbums extends React.Component {
|
||||
/>
|
||||
<DropdownField
|
||||
icon="swap_vert"
|
||||
name="Sort"
|
||||
name={i18n('fields.sort')}
|
||||
value={this.props.sort}
|
||||
valueAsLabel
|
||||
options={sort_options}
|
||||
@ -326,7 +368,7 @@ class LibraryAlbums extends React.Component {
|
||||
/>
|
||||
<DropdownField
|
||||
icon="visibility"
|
||||
name="View"
|
||||
name={i18n('fields.view')}
|
||||
value={this.props.view}
|
||||
valueAsLabel
|
||||
options={view_options}
|
||||
@ -334,7 +376,7 @@ class LibraryAlbums extends React.Component {
|
||||
/>
|
||||
<DropdownField
|
||||
icon="cloud"
|
||||
name="Source"
|
||||
name={i18n('fields.source')}
|
||||
value={this.props.source}
|
||||
valueAsLabel
|
||||
options={source_options}
|
||||
@ -347,7 +389,7 @@ class LibraryAlbums extends React.Component {
|
||||
<div className="view library-albums-view">
|
||||
<Header options={options} uiActions={this.props.uiActions}>
|
||||
<Icon name="album" type="material" />
|
||||
My albums
|
||||
<I18n path="library.albums.title" />
|
||||
</Header>
|
||||
{this.renderView()}
|
||||
</div>
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
uriSource,
|
||||
} from '../../util/helpers';
|
||||
import { sortItems, applyFilter } from '../../util/arrays';
|
||||
import { I18n, i18n } from '../../locale';
|
||||
|
||||
class LibraryArtists extends React.Component {
|
||||
constructor(props) {
|
||||
@ -38,7 +39,7 @@ class LibraryArtists extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
this.props.uiActions.setWindowTitle('Artists');
|
||||
this.props.uiActions.setWindowTitle(i18n('library.artists.title'));
|
||||
|
||||
if (!this.props.mopidy_library_artists && this.props.mopidy_connected && (this.props.source == 'all' || this.props.source == 'local')) {
|
||||
this.props.mopidyActions.getLibraryArtists();
|
||||
@ -224,55 +225,55 @@ class LibraryArtists extends React.Component {
|
||||
const source_options = [
|
||||
{
|
||||
value: 'all',
|
||||
label: 'All',
|
||||
label: i18n('fields.filters.all'),
|
||||
},
|
||||
{
|
||||
value: 'local',
|
||||
label: 'Local',
|
||||
label: i18n('services.mopidy.local'),
|
||||
},
|
||||
];
|
||||
|
||||
if (this.props.spotify_available) {
|
||||
source_options.push({
|
||||
value: 'spotify',
|
||||
label: 'Spotify',
|
||||
label: i18n('services.spotify.title'),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.google_available) {
|
||||
source_options.push({
|
||||
value: 'google',
|
||||
label: 'Google',
|
||||
label: i18n('services.google.title'),
|
||||
});
|
||||
}
|
||||
|
||||
const view_options = [
|
||||
{
|
||||
label: 'Thumbnails',
|
||||
value: 'thumbnails',
|
||||
label: i18n('fields.filters.thumbnails'),
|
||||
},
|
||||
{
|
||||
label: 'List',
|
||||
value: 'list',
|
||||
label: i18n('fields.filters.list'),
|
||||
},
|
||||
];
|
||||
|
||||
const sort_options = [
|
||||
{
|
||||
value: null,
|
||||
label: 'As loaded',
|
||||
label: i18n('fields.filters.as_loaded'),
|
||||
},
|
||||
{
|
||||
value: 'name',
|
||||
label: 'Name',
|
||||
label: i18n('fields.filters.name'),
|
||||
},
|
||||
{
|
||||
value: 'followers',
|
||||
label: 'Followers',
|
||||
label: i18n('fields.filters.followers'),
|
||||
},
|
||||
{
|
||||
value: 'popularity',
|
||||
label: 'Popularity',
|
||||
label: i18n('fields.filters.popularity'),
|
||||
},
|
||||
];
|
||||
|
||||
@ -285,7 +286,7 @@ class LibraryArtists extends React.Component {
|
||||
/>
|
||||
<DropdownField
|
||||
icon="swap_vert"
|
||||
name="Sort"
|
||||
name={i18n('fields.sort')}
|
||||
value={this.props.sort}
|
||||
valueAsLabel
|
||||
options={sort_options}
|
||||
@ -294,7 +295,7 @@ class LibraryArtists extends React.Component {
|
||||
/>
|
||||
<DropdownField
|
||||
icon="visibility"
|
||||
name="View"
|
||||
name={i18n('fields.view')}
|
||||
value={this.props.view}
|
||||
valueAsLabel
|
||||
options={view_options}
|
||||
@ -302,7 +303,7 @@ class LibraryArtists extends React.Component {
|
||||
/>
|
||||
<DropdownField
|
||||
icon="cloud"
|
||||
name="Source"
|
||||
name={i18n('fields.source')}
|
||||
value={this.props.source}
|
||||
valueAsLabel
|
||||
options={source_options}
|
||||
@ -315,7 +316,7 @@ class LibraryArtists extends React.Component {
|
||||
<div className="view library-artists-view">
|
||||
<Header options={options} uiActions={this.props.uiActions}>
|
||||
<Icon name="recent_actors" type="material" />
|
||||
My artists
|
||||
<I18n path="library.artists.title" />
|
||||
</Header>
|
||||
{this.renderView()}
|
||||
</div>
|
||||
|
||||
@ -9,11 +9,12 @@ import ErrorBoundary from '../../components/ErrorBoundary';
|
||||
import * as uiActions from '../../services/ui/actions';
|
||||
import * as mopidyActions from '../../services/mopidy/actions';
|
||||
import { formatImages } from '../../util/format';
|
||||
import { I18n, i18n } from '../../locale';
|
||||
|
||||
class LibraryBrowse extends React.Component {
|
||||
componentDidMount() {
|
||||
this.loadDirectory();
|
||||
this.props.uiActions.setWindowTitle('Browse');
|
||||
this.props.uiActions.setWindowTitle(i18n('library.browse.title'));
|
||||
}
|
||||
|
||||
componentDidUpdate = ({ mopidy_connected: prev_mopidy_connected}) => {
|
||||
@ -97,7 +98,7 @@ class LibraryBrowse extends React.Component {
|
||||
<div className="view library-local-view">
|
||||
<Header>
|
||||
<Icon name="folder" type="material" />
|
||||
Browse
|
||||
<I18n path="library.browse.title" />
|
||||
</Header>
|
||||
<section className="content-wrapper">
|
||||
<div className="grid grid--tiles">
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
isLoading,
|
||||
} from '../../util/helpers';
|
||||
import { arrayOf, sortItems } from '../../util/arrays';
|
||||
import { i18n, I18n } from '../../locale';
|
||||
|
||||
class LibraryBrowseDirectory extends React.Component {
|
||||
constructor(props) {
|
||||
@ -40,7 +41,7 @@ class LibraryBrowseDirectory extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
this.props.uiActions.setWindowTitle('Browse');
|
||||
this.props.uiActions.setWindowTitle(i18n('library.browse_directory.title'));
|
||||
this.loadDirectory();
|
||||
}
|
||||
|
||||
@ -151,7 +152,7 @@ class LibraryBrowseDirectory extends React.Component {
|
||||
} = this.props;
|
||||
const { limit } = this.state;
|
||||
|
||||
let title = 'Directory';
|
||||
let title = i18n('library.browse_directory.title');
|
||||
|
||||
if (!directory || isLoading(load_queue, ['mopidy_browse'])) {
|
||||
return (
|
||||
@ -188,12 +189,12 @@ class LibraryBrowseDirectory extends React.Component {
|
||||
|
||||
const view_options = [
|
||||
{
|
||||
label: 'Thumbnails',
|
||||
value: 'thumbnails',
|
||||
label: i18n('fields.filters.thumbnails'),
|
||||
},
|
||||
{
|
||||
label: 'List',
|
||||
value: 'list',
|
||||
label: i18n('fields.filters.list'),
|
||||
},
|
||||
];
|
||||
|
||||
@ -210,12 +211,12 @@ class LibraryBrowseDirectory extends React.Component {
|
||||
{tracks && (
|
||||
<a className="button button--no-hover" onClick={(e) => { uiActions.hideContextMenu(); this.playAll(e, all_tracks); }}>
|
||||
<Icon name="play_circle_filled" />
|
||||
Play all
|
||||
<I18n path="actions.play_all" />
|
||||
</a>
|
||||
)}
|
||||
<a className="button button--no-hover" onClick={(e) => { uiActions.hideContextMenu(); this.goBack(e); }}>
|
||||
<Icon name="keyboard_backspace" />
|
||||
Back
|
||||
<I18n path="actions.back" />
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
|
||||
@ -15,6 +15,7 @@ import * as uiActions from '../../services/ui/actions';
|
||||
import * as mopidyActions from '../../services/mopidy/actions';
|
||||
import * as spotifyActions from '../../services/spotify/actions';
|
||||
import { applyFilter, removeDuplicates, sortItems } from '../../util/arrays';
|
||||
import { I18n, i18n } from '../../locale';
|
||||
|
||||
class LibraryPlaylists extends React.Component {
|
||||
constructor(props) {
|
||||
@ -36,7 +37,7 @@ class LibraryPlaylists extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
this.props.uiActions.setWindowTitle('Playlists');
|
||||
this.props.uiActions.setWindowTitle(i18n('library.playlists.title'));
|
||||
|
||||
if (!this.props.mopidy_library_playlists && this.props.mopidy_connected && (this.props.source == 'all' || this.props.source == 'local')) {
|
||||
this.props.mopidyActions.getLibraryPlaylists();
|
||||
@ -182,60 +183,60 @@ class LibraryPlaylists extends React.Component {
|
||||
const source_options = [
|
||||
{
|
||||
value: 'all',
|
||||
label: 'All',
|
||||
label: i18n('fields.filters.all'),
|
||||
},
|
||||
{
|
||||
value: 'local',
|
||||
label: 'Local',
|
||||
label: i18n('services.mopidy.local'),
|
||||
},
|
||||
];
|
||||
|
||||
if (this.props.spotify_available) {
|
||||
source_options.push({
|
||||
value: 'spotify',
|
||||
label: 'Spotify',
|
||||
label: i18n('services.spotify.title'),
|
||||
});
|
||||
}
|
||||
|
||||
const view_options = [
|
||||
{
|
||||
value: 'thumbnails',
|
||||
label: 'Thumbnails',
|
||||
label: i18n('fields.filters.thumbnails'),
|
||||
},
|
||||
{
|
||||
value: 'list',
|
||||
label: 'List',
|
||||
label: i18n('fields.filters.list'),
|
||||
},
|
||||
];
|
||||
|
||||
const sort_options = [
|
||||
{
|
||||
value: null,
|
||||
label: 'As loaded',
|
||||
label: i18n('fields.filters.as_loaded'),
|
||||
},
|
||||
{
|
||||
value: 'name',
|
||||
label: 'Name',
|
||||
label: i18n('fields.filters.name'),
|
||||
},
|
||||
{
|
||||
value: 'last_modified',
|
||||
label: 'Updated',
|
||||
label: i18n('fields.filters.updated'),
|
||||
},
|
||||
{
|
||||
value: 'can_edit',
|
||||
label: 'Editable',
|
||||
label: i18n('fields.filters.editable'),
|
||||
},
|
||||
{
|
||||
value: 'owner.id',
|
||||
label: 'Owner',
|
||||
label: i18n('fields.filters.owner'),
|
||||
},
|
||||
{
|
||||
value: 'tracks_total',
|
||||
label: 'Tracks',
|
||||
label: i18n('fields.filters.tracks'),
|
||||
},
|
||||
{
|
||||
value: 'source',
|
||||
label: 'Source',
|
||||
label: i18n('fields.filters.source'),
|
||||
},
|
||||
];
|
||||
|
||||
@ -248,7 +249,7 @@ class LibraryPlaylists extends React.Component {
|
||||
/>
|
||||
<DropdownField
|
||||
icon="swap_vert"
|
||||
name="Sort"
|
||||
name={i18n('fields.sort')}
|
||||
value={this.props.sort}
|
||||
valueAsLabel
|
||||
options={sort_options}
|
||||
@ -257,7 +258,7 @@ class LibraryPlaylists extends React.Component {
|
||||
/>
|
||||
<DropdownField
|
||||
icon="visibility"
|
||||
name="View"
|
||||
name={i18n('fields.view')}
|
||||
valueAsLabel
|
||||
value={this.props.view}
|
||||
options={view_options}
|
||||
@ -265,7 +266,7 @@ class LibraryPlaylists extends React.Component {
|
||||
/>
|
||||
<DropdownField
|
||||
icon="cloud"
|
||||
name="Source"
|
||||
name={i18n('fields.source')}
|
||||
valueAsLabel
|
||||
value={this.props.source}
|
||||
options={source_options}
|
||||
@ -273,7 +274,7 @@ class LibraryPlaylists extends React.Component {
|
||||
/>
|
||||
<Link className="button button--no-hover" to="/playlist/create">
|
||||
<Icon name="add_box" />
|
||||
New
|
||||
<I18n path="actions.new" />
|
||||
</Link>
|
||||
</span>
|
||||
);
|
||||
@ -282,7 +283,7 @@ class LibraryPlaylists extends React.Component {
|
||||
<div className="view library-playlists-view">
|
||||
<Header options={options} uiActions={this.props.uiActions}>
|
||||
<Icon name="queue_music" type="material" />
|
||||
My playlists
|
||||
<I18n path="library.playlists.title" />
|
||||
</Header>
|
||||
{ this.renderView() }
|
||||
</div>
|
||||
|
||||
@ -11,13 +11,17 @@ import * as uiActions from '../../services/ui/actions';
|
||||
import * as mopidyActions from '../../services/mopidy/actions';
|
||||
import * as spotifyActions from '../../services/spotify/actions';
|
||||
import { isLoading } from '../../util/helpers';
|
||||
import { I18n, i18n } from '../../locale';
|
||||
|
||||
class LibraryTracks extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.uiActions.setWindowTitle('Tracks');
|
||||
this.props.uiActions.setWindowTitle(i18n('library.tracks.title'));
|
||||
|
||||
if (!this.props.spotify_available) {
|
||||
this.props.uiActions.createNotification({ level: 'warning', content: 'Enable Spotify to browse tracks' });
|
||||
this.props.uiActions.createNotification({
|
||||
level: 'warning',
|
||||
content: i18n('errors.enable_first', { provider: i18n('services.spotify.title') }),
|
||||
});
|
||||
} else if (this.props.library_tracks === undefined) {
|
||||
this.props.spotifyActions.getLibraryTracks();
|
||||
}
|
||||
@ -42,7 +46,7 @@ class LibraryTracks extends React.Component {
|
||||
if (isLoading(this.props.load_queue, ['spotify_me/tracks?'])) {
|
||||
return (
|
||||
<div className="view library-tracks-view">
|
||||
<Header icon="music" title="My tracks" />
|
||||
<Header icon="music" title={i18n('library.tracks.title')} />
|
||||
<Loader body loading />
|
||||
</div>
|
||||
);
|
||||
@ -61,7 +65,7 @@ class LibraryTracks extends React.Component {
|
||||
const options = (
|
||||
<a className="button button--no-hover" onClick={(e) => this.playAll(e)}>
|
||||
<Icon name="play_circle_filled" />
|
||||
Play all
|
||||
<I18n path="actions.play_all" />
|
||||
</a>
|
||||
);
|
||||
|
||||
@ -69,7 +73,7 @@ Play all
|
||||
<div className="view library-tracks-view">
|
||||
<Header options={options} uiActions={this.props.uiActions}>
|
||||
<Icon name="music_note" type="material" />
|
||||
My tracks
|
||||
<I18n path="library.tracks.title" />
|
||||
</Header>
|
||||
<section className="content-wrapper">
|
||||
<TrackList tracks={tracks} />
|
||||
|
||||
@ -12,6 +12,7 @@ import * as mopidyActions from '../../services/mopidy/actions';
|
||||
import * as spotifyActions from '../../services/spotify/actions';
|
||||
import { sourceIcon, decodeMopidyUri } from '../../util/helpers';
|
||||
import { sortItems } from '../../util/arrays';
|
||||
import { I18n } from '../../locale';
|
||||
|
||||
class AddToPlaylist extends React.Component {
|
||||
componentDidMount = () => {
|
||||
@ -48,7 +49,11 @@ class AddToPlaylist extends React.Component {
|
||||
render = () =>{
|
||||
const { playlists, uris, spotify_library_playlists_status } = this.props;
|
||||
|
||||
if (!playlists) return <div className="empty">No editable playlists</div>;
|
||||
if (!playlists) return (
|
||||
<div className="empty">
|
||||
<I18n path="modal.add_to_playlist.no_editable_playlists" />
|
||||
</div>
|
||||
);
|
||||
|
||||
let editablePlaylists = [];
|
||||
for (let uri in playlists) {
|
||||
@ -61,11 +66,19 @@ class AddToPlaylist extends React.Component {
|
||||
|
||||
return (
|
||||
<Modal className="modal--add-to-playlist">
|
||||
<h1>Add to playlist</h1>
|
||||
<h1><I18n path="modal.add_to_playlist.title" /></h1>
|
||||
<h2 className="mid_grey-text">
|
||||
{`Select playlist to add ${uris.length} track ${uris.length > 1 ? 's' : ''} to`}
|
||||
<I18n
|
||||
path="modal.add_to_playlist.subtitle"
|
||||
count={uris.length}
|
||||
plural={uris.length > 1 ? 's' : ''}
|
||||
/>
|
||||
</h2>
|
||||
{editablePlaylists.length <= 0 && <div className="no-results">No playlists available</div>}
|
||||
{editablePlaylists.length <= 0 && (
|
||||
<div className="no-results">
|
||||
<I18n path="modal.add_to_playlist.no_playlists" />
|
||||
</div>
|
||||
)}
|
||||
<div className="list small playlists">
|
||||
{editablePlaylists.map((playlist) => (
|
||||
<div
|
||||
|
||||
@ -7,6 +7,7 @@ import * as coreActions from '../../services/core/actions';
|
||||
import * as uiActions from '../../services/ui/actions';
|
||||
import * as mopidyActions from '../../services/mopidy/actions';
|
||||
import * as spotifyActions from '../../services/spotify/actions';
|
||||
import { I18n, i18n } from '../../locale';
|
||||
|
||||
class AddToQueue extends React.Component {
|
||||
constructor(props) {
|
||||
@ -18,7 +19,7 @@ class AddToQueue extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.uiActions.setWindowTitle('Add to queue');
|
||||
this.props.uiActions.setWindowTitle(i18n('modal.add_to_queue.title'));
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
@ -31,12 +32,18 @@ class AddToQueue extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Modal className="modal--add-to-queue">
|
||||
<h1>Add to queue</h1>
|
||||
<h2 className="mid_grey-text">Add a comma-separated list of URIs to the play queue. You must have the appropriate Mopidy backend enabled for each URI schema (eg spotify:, yt:).</h2>
|
||||
<h1>
|
||||
<I18n path="modal.add_to_queue.title"/>
|
||||
</h1>
|
||||
<h2 className="mid_grey-text">
|
||||
<I18n path="modal.add_to_queue.subtitle" />
|
||||
</h2>
|
||||
|
||||
<form onSubmit={(e) => this.handleSubmit(e)}>
|
||||
<div className="field text">
|
||||
<div className="name">URI(s)</div>
|
||||
<div className="name">
|
||||
<I18n path="modal.add_to_queue.uris" />
|
||||
</div>
|
||||
<div className="input">
|
||||
<input
|
||||
type="text"
|
||||
@ -48,7 +55,7 @@ class AddToQueue extends React.Component {
|
||||
|
||||
<div className="field radio white">
|
||||
<div className="name">
|
||||
Position
|
||||
<I18n path="modal.add_to_queue.position.label" />
|
||||
</div>
|
||||
<div className="input">
|
||||
<label>
|
||||
@ -58,7 +65,9 @@ class AddToQueue extends React.Component {
|
||||
checked={!this.state.next}
|
||||
onChange={(e) => this.setState({ next: false })}
|
||||
/>
|
||||
<span className="label">End</span>
|
||||
<span className="label">
|
||||
<I18n path="modal.add_to_queue.position.end" />
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
@ -67,13 +76,17 @@ class AddToQueue extends React.Component {
|
||||
checked={this.state.next}
|
||||
onChange={(e) => this.setState({ next: true })}
|
||||
/>
|
||||
<span className="label">After current track</span>
|
||||
<span className="label">
|
||||
<I18n path="modal.add_to_queue.position.next" />
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="actions centered-text">
|
||||
<button type="submit" className="button button--primary button--large">Add</button>
|
||||
<button type="submit" className="button button--primary button--large">
|
||||
<I18n path="actions.add" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user