All views done this time; Language file mapping for modals.. Will need to do destructuring in second sweep

This commit is contained in:
James Barnsley
2020-07-19 20:41:14 +12:00
parent 9f186d281c
commit 2de98afee4
21 changed files with 1807 additions and 1330 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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" />
&nbsp;
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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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