Renaming Content to I18n to avoid naming clashes; Artist and Debug views mapped

This commit is contained in:
James Barnsley
2020-07-07 20:52:09 +12:00
parent fca28ada88
commit 6225c6a6b0
18 changed files with 1058 additions and 917 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -100,7 +100,7 @@
// Release details // Release details
// These are automatically injected to built HTML // These are automatically injected to built HTML
var build = "1594021564"; var build = "1594107635";
var version = "3.50.0"; var version = "3.50.0";
// Construct the script tag // Construct the script tag

View File

@ -1,6 +1,6 @@
import { memo } from 'react'; import { memo } from 'react';
import { content } from '../locale'; import { i18n } from '../locale';
/** /**
* Format time duration * Format time duration
@ -51,10 +51,10 @@ const durationSentence = (milliseconds = null) => {
const totalMinutes = Math.floor(milliseconds / (1000 * 60)); const totalMinutes = Math.floor(milliseconds / (1000 * 60));
const totalHours = Math.floor(milliseconds / (1000 * 60 * 60)); const totalHours = Math.floor(milliseconds / (1000 * 60 * 60));
if (totalHours > 1) return `${totalHours}+ ${content('time.hours.short')}`; if (totalHours > 1) return `${totalHours}+ ${i18n('time.hours.short')}`;
if (totalMinutes > 1) return `${totalMinutes} ${content('time.minutes.short')}`; if (totalMinutes > 1) return `${totalMinutes} ${i18n('time.minutes.short')}`;
if (totalSeconds) return `${totalSeconds} ${content('time.seconds.short')}`; if (totalSeconds) return `${totalSeconds} ${i18n('time.seconds.short')}`;
return `0 ${content('time.minutes.short')}`; return `0 ${i18n('time.minutes.short')}`;
}; };
const dater = (type, data) => { const dater = (type, data) => {
@ -99,17 +99,17 @@ const dater = (type, data) => {
var years = Math.floor(diff / (1000 * 60 * 60 * 24 * 7 * 52)); var years = Math.floor(diff / (1000 * 60 * 60 * 24 * 7 * 52));
if (seconds < 60) { if (seconds < 60) {
return `${seconds} ${content(`time.seconds.${seconds > 1 ? 'singular' : 'plural'}`)}`; return `${seconds} ${i18n(`time.seconds.${seconds > 1 ? 'singular' : 'plural'}`)}`;
} if (minutes < 60) { } if (minutes < 60) {
return `${minutes} ${content(`time.minutes.${minutes > 1 ? 'singular' : 'plural'}`)}`; return `${minutes} ${i18n(`time.minutes.${minutes > 1 ? 'singular' : 'plural'}`)}`;
} if (hours < 24) { } if (hours < 24) {
return `${hours} ${content(`time.hours.${hours > 1 ? 'singular' : 'plural'}`)}`; return `${hours} ${i18n(`time.hours.${hours > 1 ? 'singular' : 'plural'}`)}`;
} if (days < 7) { } if (days < 7) {
return `${days} ${content(`time.days.${days > 1 ? 'singular' : 'plural'}`)}`; return `${days} ${i18n(`time.days.${days > 1 ? 'singular' : 'plural'}`)}`;
} if (weeks < 54) { } if (weeks < 54) {
return `${weeks} ${content(`time.weeks.${weeks > 1 ? 'singular' : 'plural'}`)}`; return `${weeks} ${i18n(`time.weeks.${weeks > 1 ? 'singular' : 'plural'}`)}`;
} }
return `${years} ${content(`time.years.${years > 1 ? 'singular' : 'plural'}`)}`; return `${years} ${i18n(`time.years.${years > 1 ? 'singular' : 'plural'}`)}`;
default: default:
return null; return null;
} }

View File

@ -5,19 +5,20 @@ import { bindActionCreators } from 'redux';
import * as uiActions from '../../services/ui/actions'; import * as uiActions from '../../services/ui/actions';
import * as spotifyActions from '../../services/spotify/actions'; import * as spotifyActions from '../../services/spotify/actions';
import { isLoading, getFromUri } from '../../util/helpers'; import { isLoading, getFromUri } from '../../util/helpers';
import { i18n } from '../../locale';
class FollowButton extends React.Component { class FollowButton extends React.Component {
remove() { remove = () => {
const { spotifyActions: actions, uri } = this.props; const { spotifyActions: actions, uri } = this.props;
actions.following(uri, 'DELETE'); actions.following(uri, 'DELETE');
} }
add() { add = () => {
const { spotifyActions: actions, uri } = this.props; const { spotifyActions: actions, uri } = this.props;
actions.following(uri, 'PUT'); actions.following(uri, 'PUT');
} }
render() { render = () => {
const { const {
uri, uri,
addText, addText,
@ -49,7 +50,7 @@ class FollowButton extends React.Component {
className={`${className} button--disabled`} className={`${className} button--disabled`}
onClick={() => actions.createNotification({ content: 'You must authorize Spotify first', level: 'warning' })} onClick={() => actions.createNotification({ content: 'You must authorize Spotify first', level: 'warning' })}
> >
{addText} {addText || i18n('actions.add_to_library')}
</button> </button>
); );
} if (is_following === true) { } if (is_following === true) {
@ -57,9 +58,9 @@ class FollowButton extends React.Component {
<button <button
type="button" type="button"
className={`${className} button--destructive`} className={`${className} button--destructive`}
onClick={() => this.remove()} onClick={this.remove}
> >
{removeText} {removeText || i18n('actions.remove_from_library')}
</button> </button>
); );
} }
@ -67,15 +68,15 @@ class FollowButton extends React.Component {
<button <button
type="button" type="button"
className={`${className} button--default`} className={`${className} button--default`}
onClick={() => this.add()} onClick={this.add}
> >
{addText} {addText || i18n('actions.add_to_library')}
</button> </button>
); );
} }
} }
const mapStateToProps = (state, ownProps) => ({ const mapStateToProps = (state) => ({
load_queue: state.ui.load_queue, load_queue: state.ui.load_queue,
spotify_authorized: state.spotify.authorization, spotify_authorized: state.spotify.authorization,
}); });

View File

@ -2,7 +2,7 @@
import React from 'react'; import React from 'react';
import LinksSentence from './LinksSentence'; import LinksSentence from './LinksSentence';
import Dater from './Dater'; import { Dater } from './Dater';
import URILink from './URILink'; import URILink from './URILink';
import ContextMenuTrigger from './ContextMenuTrigger'; import ContextMenuTrigger from './ContextMenuTrigger';
import Icon from './Icon'; import Icon from './Icon';

View File

@ -8,7 +8,7 @@ import ProgressSlider from './Fields/ProgressSlider';
import VolumeControl from './Fields/VolumeControl'; import VolumeControl from './Fields/VolumeControl';
import MuteControl from './Fields/MuteControl'; import MuteControl from './Fields/MuteControl';
import OutputControl from './Fields/OutputControl'; import OutputControl from './Fields/OutputControl';
import Dater from './Dater'; import { Dater } from './Dater';
import LinksSentence from './LinksSentence'; import LinksSentence from './LinksSentence';
import Thumbnail from './Thumbnail'; import Thumbnail from './Thumbnail';
import Icon from './Icon'; import Icon from './Icon';

View File

@ -8,7 +8,7 @@ import Icon from './Icon';
import Dropzones from './Fields/Dropzones'; import Dropzones from './Fields/Dropzones';
import * as uiActions from '../services/ui/actions'; import * as uiActions from '../services/ui/actions';
import * as mopidyActions from '../services/mopidy/actions'; import * as mopidyActions from '../services/mopidy/actions';
import { content, Content } from '../locale'; import { I18n } from '../locale';
class Sidebar extends React.Component { class Sidebar extends React.Component {
closeSidebar = () => uiActions.toggleSidebar(false); closeSidebar = () => uiActions.toggleSidebar(false);
@ -47,19 +47,19 @@ class Sidebar extends React.Component {
<Icon name="warning" className="red-text" /> <Icon name="warning" className="red-text" />
<span className="tooltip__content"> <span className="tooltip__content">
{!mopidy_connected && ( {!mopidy_connected && (
<Content path="sidebar.not_connected" params={{ name: 'Mopidy' }}> <I18n path="sidebar.not_connected" params={{ name: 'Mopidy' }}>
<br /> <br />
</Content> </I18n>
)} )}
{!pusher_connected && ( {!pusher_connected && (
<Content path="sidebar.not_connected" params={{ name: 'Pusher' }}> <I18n path="sidebar.not_connected" params={{ name: 'Pusher' }}>
<br /> <br />
</Content> </I18n>
)} )}
{!snapcast_connected && snapcast_enabled && ( {!snapcast_connected && snapcast_enabled && (
<Content path="sidebar.not_connected" params={{ name: 'Snapcast' }}> <I18n path="sidebar.not_connected" params={{ name: 'Snapcast' }}>
<br /> <br />
</Content> </I18n>
)} )}
</span> </span>
</span> </span>
@ -84,68 +84,68 @@ class Sidebar extends React.Component {
<section className="sidebar__menu__section"> <section className="sidebar__menu__section">
<Link to="/queue" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active"> <Link to="/queue" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="play_arrow" type="material" /> <Icon name="play_arrow" type="material" />
<Content path="sidebar.now_playing" /> <I18n path="sidebar.now_playing" />
</Link> </Link>
<Link to="/search" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active"> <Link to="/search" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="search" type="material" /> <Icon name="search" type="material" />
<Content path="sidebar.search" /> <I18n path="sidebar.search" />
</Link> </Link>
</section> </section>
{spotify_available && ( {spotify_available && (
<section className="sidebar__menu__section"> <section className="sidebar__menu__section">
<title className="sidebar__menu__section__title"> <title className="sidebar__menu__section__title">
<Content path="sidebar.discover" /> <I18n path="sidebar.discover" />
</title> </title>
<Link to="/discover/recommendations" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active"> <Link to="/discover/recommendations" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="explore" type="material" /> <Icon name="explore" type="material" />
<Content path="sidebar.discover" /> <I18n path="sidebar.discover" />
</Link> </Link>
<Link to="/discover/categories" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active"> <Link to="/discover/categories" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="mood" type="material" /> <Icon name="mood" type="material" />
<Content path="sidebar.genre" /> <I18n path="sidebar.genre" />
</Link> </Link>
<Link to="/discover/featured" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active"> <Link to="/discover/featured" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="star" type="material" /> <Icon name="star" type="material" />
<Content path="sidebar.featured_playlists" /> <I18n path="sidebar.featured_playlists" />
</Link> </Link>
<Link to="/discover/new-releases" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active"> <Link to="/discover/new-releases" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="new_releases" type="material" /> <Icon name="new_releases" type="material" />
<Content path="sidebar.new_releases" /> <I18n path="sidebar.new_releases" />
</Link> </Link>
</section> </section>
)} )}
<section className="sidebar__menu__section"> <section className="sidebar__menu__section">
<title className="sidebar__menu__section__title"> <title className="sidebar__menu__section__title">
<Content path="sidebar.my_music" /> <I18n path="sidebar.my_music" />
</title> </title>
<Link to="/library/playlists" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active"> <Link to="/library/playlists" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="queue_music" type="material" /> <Icon name="queue_music" type="material" />
<Content path="sidebar.playlists" /> <I18n path="sidebar.playlists" />
</Link> </Link>
<Link to="/library/artists" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active"> <Link to="/library/artists" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="recent_actors" type="material" /> <Icon name="recent_actors" type="material" />
<Content path="sidebar.artists" /> <I18n path="sidebar.artists" />
</Link> </Link>
<Link to="/library/albums" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active"> <Link to="/library/albums" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="album" type="material" /> <Icon name="album" type="material" />
<Content path="sidebar.albums" /> <I18n path="sidebar.albums" />
</Link> </Link>
<Link to="/library/tracks" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active"> <Link to="/library/tracks" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="music_note" type="material" /> <Icon name="music_note" type="material" />
<Content path="sidebar.tracks" /> <I18n path="sidebar.tracks" />
</Link> </Link>
<Link to="/library/browse" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active"> <Link to="/library/browse" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="folder" type="material" /> <Icon name="folder" type="material" />
<Content path="sidebar.browse" /> <I18n path="sidebar.browse" />
</Link> </Link>
</section> </section>
<section className="sidebar__menu__section"> <section className="sidebar__menu__section">
<Link to="/settings" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active"> <Link to="/settings" history={history} className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
<Icon name="settings" type="material" /> <Icon name="settings" type="material" />
<Content path="sidebar.settings" /> <I18n path="sidebar.settings" />
{this.renderStatusIcon()} {this.renderStatusIcon()}
</Link> </Link>
</section> </section>

View File

@ -2,7 +2,7 @@
import React from 'react'; import React from 'react';
import Icon from './Icon'; import Icon from './Icon';
import LinksSentence from './LinksSentence'; import LinksSentence from './LinksSentence';
import Dater from './Dater'; import { Dater } from './Dater';
import URILink from './URILink'; import URILink from './URILink';
import ContextMenuTrigger from './ContextMenuTrigger'; import ContextMenuTrigger from './ContextMenuTrigger';
import ErrorBoundary from './ErrorBoundary'; import ErrorBoundary from './ErrorBoundary';

View File

@ -1,16 +1,22 @@
errors: errors:
uri_not_found: Could not find anything with URI %{uri} uri_not_found: Could not find anything with URI %{uri}
specs: specs:
tracks: %{count} tracks tracks: '%{count} tracks'
followers: %{count} followers followers: '%{count} followers'
listeners: %{count} listeners listeners: '%{count} listeners'
plays: %{count} plays plays: '%{count} plays'
popularity: '%{percent}% popularity'
actions: actions:
play: Play play: Play
pause: Pause pause: Pause
stop: Stop stop: Stop
add_to_library: Add to library add_to_library: Add to library
remove_from_library: Remove from library remove_from_library: Remove from library
reset: Reset
back: Back
enable: Enable
disable: Disable
send: Send
time: time:
seconds: seconds:
short: secs short: secs
@ -36,6 +42,13 @@ time:
short: yrs short: yrs
singular: year singular: year
plural: years plural: years
services:
mopidy:
title: Mopidy
pusher:
title: Pusher
snapcast:
title: Snapcast
sidebar: sidebar:
now_playing: Now playing now_playing: Now playing
search: Search search: Search
@ -50,7 +63,7 @@ sidebar:
tracks: Tracks tracks: Tracks
browse: Browse browse: Browse
settings: Settings settings: Settings
not_connected: %{name} not connected not_connected: '%{name} not connected'
now_playing: now_playing:
title: Now playing title: Now playing
context_actions: context_actions:
@ -64,25 +77,63 @@ now_playing:
radio: Radio radio: Radio
album: album:
title: Album title: Album
title_window: %{name} by %{artist} (album) title_window: '%{name} by %{artist} (album)'
wiki: wiki:
title: About title: About
published: 'Published: %{date}' published: 'Published: %{date}'
artist:
title: Artist
title_window: '%{name} (artist)'
overview:
title: Overview
top_tracks: Top tracks
related_artists:
title: Related artists
more: All related artists
tracks:
title: Tracks
related_artists:
title: Related artists
about:
title: About
source: '%{source} artist'
wiki:
published: 'Published: %{date}'
origin: 'Origin: '
debug:
title: Debug
ui:
title: User interface
debug:
title: Debug
overlay: Show debug overlay
logging:
title: Logging
mopidy: Log Mopidy
pusher: Log Pusher
actions: Log Actions
snapcast: Log Snapcast
create_notification: Create notification
create_process: Create process notification
run_test: Run test process
call: Call
data: Data
examples: Examples
search: search:
title: 'Search: %{term}' title: 'Search: %{term}'
context_actions: context_actions:
sort: Sort sort: Sort
source: Sources (%{count}) source: 'Sources (%{count})'
placeholder: Search placeholder: Search
artists: artists:
title: Artists title: Artists
more: All artists (%{count}) more: 'All artists (%{count})'
albums: albums:
title: Albums title: Albums
more: All albums (%{count}) more: 'All albums (%{count})'
playlists: playlists:
title: Playlists title: Playlists
more: All playlists (%{count}) more: 'All playlists (%{count})'
tracks: tracks:
title: Tracks title: Tracks
settings: settings:

View File

@ -1,5 +1,4 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import { useSelector } from 'react-redux';
import get from 'lodash/get'; import get from 'lodash/get';
import dictionaries from './dictionaries'; import dictionaries from './dictionaries';
import { titleCase } from '../util/helpers'; import { titleCase } from '../util/helpers';
@ -7,7 +6,7 @@ import { titleCase } from '../util/helpers';
const PARAMS_REG_EXP = '%{(.*?)}'; const PARAMS_REG_EXP = '%{(.*?)}';
const paramsRegExp = new RegExp(PARAMS_REG_EXP, 'g'); const paramsRegExp = new RegExp(PARAMS_REG_EXP, 'g');
const content = (path, params = {}, transform) => { const i18n = (path, params = {}, transform) => {
const dictionary = dictionaries[window.language || 'en'] || dictionaries.en; const dictionary = dictionaries[window.language || 'en'] || dictionaries.en;
let value = get((dictionary), path, ''); let value = get((dictionary), path, '');
@ -33,19 +32,19 @@ const content = (path, params = {}, transform) => {
return value; return value;
}; };
const Content = ({ path, params = {}, transform, children }) => ( const I18n = ({ path, transform, children, ...params }) => (
<Fragment> <Fragment>
{content(path, params, transform)} {i18n(path, params, transform)}
{children} {children}
</Fragment> </Fragment>
); );
export default { export default {
Content, I18n,
content, i18n,
}; };
export { export {
Content, I18n,
content, i18n,
}; };

View File

@ -8,12 +8,12 @@ import Thumbnail from '../components/Thumbnail';
import LinksSentence from '../components/LinksSentence'; import LinksSentence from '../components/LinksSentence';
import Loader from '../components/Loader'; import Loader from '../components/Loader';
import FollowButton from '../components/Fields/FollowButton'; import FollowButton from '../components/Fields/FollowButton';
import NiceNumber, { nice_number } from '../components/NiceNumber'; import { nice_number } from '../components/NiceNumber';
import Dater from '../components/Dater'; import { Dater } from '../components/Dater';
import LazyLoadListener from '../components/LazyLoadListener'; import LazyLoadListener from '../components/LazyLoadListener';
import ContextMenuTrigger from '../components/ContextMenuTrigger'; import ContextMenuTrigger from '../components/ContextMenuTrigger';
import Icon from '../components/Icon'; import Icon from '../components/Icon';
import { content } from '../locale'; import { i18n, I18n } from '../locale';
import * as coreActions from '../services/core/actions'; import * as coreActions from '../services/core/actions';
import * as uiActions from '../services/ui/actions'; import * as uiActions from '../services/ui/actions';
import * as mopidyActions from '../services/mopidy/actions'; import * as mopidyActions from '../services/mopidy/actions';
@ -111,9 +111,9 @@ export class Album extends React.Component {
} }
} }
} }
setWindowTitle(content('album.title_window', { name: album.name, artist: artistNames.join() })); setWindowTitle(i18n('album.title_window', { name: album.name, artist: artistNames.join() }));
} else { } else {
setWindowTitle(content('album.title')); setWindowTitle(i18n('album.title'));
} }
} }
@ -174,6 +174,7 @@ export class Album extends React.Component {
tracks, tracks,
artists, artists,
load_queue, load_queue,
slim_mode,
} = this.props; } = this.props;
if (!albumProp) { if (!albumProp) {
@ -187,7 +188,7 @@ export class Album extends React.Component {
return ( return (
<ErrorMessage type="not-found" title="Not found"> <ErrorMessage type="not-found" title="Not found">
<p> <p>
{content('errors.uri_not_found', { uri: encodeURIComponent(uri) })} {i18n('errors.uri_not_found', { uri: encodeURIComponent(uri) })}
</p> </p>
</ErrorMessage> </ErrorMessage>
); );
@ -195,15 +196,11 @@ export class Album extends React.Component {
const album = collate(albumProp, { tracks, artists }); const album = collate(albumProp, { tracks, artists });
if ( const is_loading_tracks = (
!album.tracks_uris !album.tracks_uris
|| (album.tracks_uris && !album.tracks) || (album.tracks_uris && !album.tracks)
|| album.tracks_uris.length !== album.tracks.length || album.tracks_uris.length !== album.tracks.length
) { );
var is_loading_tracks = true;
} else {
var is_loading_tracks = false;
}
return ( return (
<div className="view album-view content-wrapper preserve-3d"> <div className="view album-view content-wrapper preserve-3d">
@ -215,7 +212,7 @@ export class Album extends React.Component {
<h1>{album.name}</h1> <h1>{album.name}</h1>
<ul className="details"> <ul className="details">
{!this.props.slim_mode ? ( {!slim_mode ? (
<li className="source"> <li className="source">
<Icon type="fontawesome" name={sourceIcon(album.uri)} /> <Icon type="fontawesome" name={sourceIcon(album.uri)} />
</li> </li>
@ -232,28 +229,28 @@ export class Album extends React.Component {
) : null} ) : null}
{album.tracks ? ( {album.tracks ? (
<li> <li>
{content( {i18n(
'specs.tracks', 'specs.tracks',
{ count: album.tracks_total || album.tracks.length }, { count: album.tracks_total || album.tracks.length },
)} )}
</li> </li>
) : null} ) : null}
{!this.props.slim_mode && album.tracks ? ( {!slim_mode && album.tracks ? (
<li> <li>
<Dater type="total-time" data={album.tracks} /> <Dater type="total-time" data={album.tracks} />
</li> </li>
) : null} ) : null}
{!this.props.slim_mode && album.play_count ? ( {!slim_mode && album.play_count ? (
<li> <li>
{content( {i18n(
'specs.plays', 'specs.plays',
{ count: nice_number(album.play_count) }, { count: nice_number(album.play_count) },
)} )}
</li> </li>
) : null} ) : null}
{!this.props.slim_mode && album.listeners ? ( {!slim_mode && album.listeners ? (
<li> <li>
{content( {i18n(
'specs.plays', 'specs.plays',
{ count: nice_number(album.listeners) }, { count: nice_number(album.listeners) },
)} )}
@ -263,19 +260,17 @@ export class Album extends React.Component {
</div> </div>
<div className="actions"> <div className="actions">
<button type="button" className="button button--primary" onClick={(e) => this.play()}> <button type="button" className="button button--primary" onClick={this.play}>
{content('actions.play')} <I18n path="actions.play" />
</button> </button>
{uriSource(this.props.uri) == 'spotify' ? ( {uriSource(uri) === 'spotify' && (
<FollowButton <FollowButton
className="secondary" className="secondary"
uri={this.props.uri} uri={uri}
addText={content('actions.add_to_library')}
removeText={content('actions.remove_from_library')}
is_following={this.inLibrary()} is_following={this.inLibrary()}
/> />
) : null} )}
<ContextMenuTrigger onTrigger={(e) => this.handleContextMenu(e)} /> <ContextMenuTrigger onTrigger={this.handleContextMenu} />
</div> </div>
<section className="list-wrapper"> <section className="list-wrapper">
@ -287,21 +282,18 @@ export class Album extends React.Component {
<LazyLoadListener <LazyLoadListener
loadKey={album.tracks_more} loadKey={album.tracks_more}
showLoader={is_loading_tracks} showLoader={is_loading_tracks}
loadMore={() => this.loadMore()} loadMore={this.loadMore}
/> />
</section> </section>
{album.wiki ? ( {album.wiki ? (
<section className="wiki"> <section className="wiki">
<h4 className="wiki__title">{content('album.wiki.title')}</h4> <h4 className="wiki__title">{i18n('album.wiki.title')}</h4>
<div className="wiki__text"> <div className="wiki__text">
<p>{album.wiki}</p> <p>{album.wiki}</p>
<br /> <br />
<div className="mid_grey-text"> <div className="mid_grey-text">
{content( <I18n path="album.wiki.published" params={{ date: album.wiki_publish_date }} />
'album.wiki.published',
{ date: album.wiki_publish_date },
)}
</div> </div>
</div> </div>
</section> </section>

View File

@ -32,6 +32,7 @@ import {
} from '../util/helpers'; } from '../util/helpers';
import { collate } from '../util/format'; import { collate } from '../util/format';
import { sortItems, applyFilter } from '../util/arrays'; import { sortItems, applyFilter } from '../util/arrays';
import { i18n, I18n } from '../locale';
class Artist extends React.Component { class Artist extends React.Component {
componentDidMount() { componentDidMount() {
@ -56,7 +57,7 @@ class Artist extends React.Component {
if (uri !== prevUri) { if (uri !== prevUri) {
loadArtist(uri); loadArtist(uri);
} else if (!prev_mopidy_connected && mopidy_connected) { } else if (!prev_mopidy_connected && mopidy_connected) {
if (uriSource(uri) != 'spotify') { if (uriSource(uri) !== 'spotify') {
loadArtist(uri); loadArtist(uri);
} }
} }
@ -65,74 +66,127 @@ class Artist extends React.Component {
if (prevUri !== uri && artist) this.setWindowTitle(artist); if (prevUri !== uri && artist) this.setWindowTitle(artist);
} }
setWindowTitle(artist = this.props.artist) { onResetFilters = () => {
this.setFilter(null);
this.setSort(null);
}
onChangeFilter = (value) => {
const { uiActions: { set, hideContextMenu } } = this.props;
set({ artist_albums_filter: value });
hideContextMenu();
}
onChangeSort = (value) => {
const {
sort,
sort_reverse,
uiActions: {
set,
hideContextMenu,
},
} = this.props;
let reverse = false;
if (value !== null && sort == value) {
reverse = !sort_reverse;
}
set({
artist_albums_sort_reverse: reverse,
artist_albums_sort: value,
});
hideContextMenu();
}
onPlay = () => {
const {
artist: {
uri,
tracks_uris,
albums_uris,
},
mopidyActions: { playURIs },
} = this.props;
playURIs(tracks_uris || albums_uris, uri);
}
setWindowTitle = (artist = this.props.artist) => {
const { uiActions: { setWindowTitle } } = this.props;
if (artist) { if (artist) {
this.props.uiActions.setWindowTitle(`${artist.name} (artist)`); setWindowTitle(i18n('artist.title_window', { name: artist.name }));
} else { } else {
this.props.uiActions.setWindowTitle('Artist'); setWindowTitle(i18n('artist.title'));
} }
} }
handleContextMenu(e) { handleContextMenu = (e) => {
const data = { const {
artist,
uri,
uiActions: { showContextMenu },
} = this.props;
showContextMenu({
e, e,
context: 'artist', context: 'artist',
items: [this.props.artist], items: [artist],
uris: [this.props.uri], uris: [uri],
}; });
this.props.uiActions.showContextMenu(data);
} }
loadMore() { loadMore = () => {
this.props.spotifyActions.getMore( const {
this.props.artist.albums_more, spotifyActions: { getMore },
artist: { albums_more },
uri,
} = this.props;
getMore(
albums_more,
{ {
parent_type: 'artist', parent_type: 'artist',
parent_key: this.props.uri, parent_key: uri,
records_type: 'album', records_type: 'album',
}, },
); );
} }
inLibrary() { inLibrary = () => {
const library = `${uriSource(this.props.uri)}_library_artists`; const { uri } = this.props;
return (this.props[library] && this.props[library].indexOf(this.props.uri) > -1); const libraryName = `${uriSource(uri)}_library_artists`;
const { [libraryName]: library = [] } = this.props;
return library.indexOf(uri) > -1;
} }
setSort(value) { renderOverview = () => {
let reverse = false; const {
if (value !== null && this.props.sort == value) { uri,
reverse = !this.props.sort_reverse; uiActions,
} artist: artistProp,
artists,
const data = { albums,
artist_albums_sort_reverse: reverse, tracks,
artist_albums_sort: value, sort,
}; sort_reverse,
this.props.uiActions.set(data); filter,
} } = this.props;
setFilter(value) {
this.props.uiActions.set({ artist_albums_filter: value });
}
renderOverview() {
const { uiActions } = this.props;
const artist = collate( const artist = collate(
this.props.artist, artistProp,
{ {
artists: this.props.artists, artists,
albums: this.props.albums, albums,
tracks: this.props.tracks, tracks,
}, },
); );
if (this.props.sort && artist.albums) { if (sort && artist.albums) {
artist.albums = sortItems(artist.albums, this.props.sort, this.props.sort_reverse); artist.albums = sortItems(artist.albums, sort, sort_reverse);
} }
if (this.props.filter && artist.albums) { if (filter && artist.albums) {
artist.albums = applyFilter('type', this.props.filter, artist.albums); artist.albums = applyFilter('type', filter, artist.albums);
} }
const sort_options = [ const sort_options = [
@ -173,16 +227,16 @@ class Artist extends React.Component {
}, },
]; ];
if (!artist.tracks_uris || (artist.tracks_uris && !artist.tracks) || (artist.tracks_uris.length !== artist.tracks.length)) { const is_loading_tracks = (
var is_loading_tracks = true; !artist.tracks_uris
} else { || (artist.tracks_uris && !artist.tracks)
var is_loading_tracks = false; || (artist.tracks_uris.length !== artist.tracks.length)
} );
return ( return (
<div className="body overview"> <div className="body overview">
<div className={`top-tracks col col--w${artist.related_artists && artist.related_artists.length > 0 ? '70' : '100'}`}> <div className={`top-tracks col col--w${artist.related_artists && artist.related_artists.length > 0 ? '70' : '100'}`}>
{artist.tracks ? <h4>Top tracks</h4> : null} {artist.tracks && <h4><I18n path="artist.overview.top_tracks" /></h4>}
<div className="list-wrapper"> <div className="list-wrapper">
<TrackList className="artist-track-list" uri={artist.uri} tracks={artist.tracks ? artist.tracks.splice(0, 10) : []} /> <TrackList className="artist-track-list" uri={artist.uri} tracks={artist.tracks ? artist.tracks.splice(0, 10) : []} />
<LazyLoadListener showLoader={is_loading_tracks} /> <LazyLoadListener showLoader={is_loading_tracks} />
@ -193,7 +247,7 @@ class Artist extends React.Component {
{artist.related_artists && artist.related_artists.length > 0 && ( {artist.related_artists && artist.related_artists.length > 0 && (
<div className="col col--w25 related-artists"> <div className="col col--w25 related-artists">
<h4>Related artists</h4> <h4><I18n path="artist.overview.related_artists.title" /></h4>
<div className="list-wrapper"> <div className="list-wrapper">
<RelatedArtists <RelatedArtists
artists={artist.related_artists.slice(0, 6)} artists={artist.related_artists.slice(0, 6)}
@ -201,11 +255,11 @@ class Artist extends React.Component {
/> />
</div> </div>
<Link <Link
to={`/artist/${encodeURIComponent(this.props.uri)}/related-artists`} to={`/artist/${encodeURIComponent(uri)}/related-artists`}
scrollTo="#sub-views-menu" scrollTo="#sub-views-menu"
className="button button--default" className="button button--default"
> >
All related artists <I18n path="artist.overview.related_artists.more" />
</Link> </Link>
</div> </div>
)} )}
@ -215,28 +269,28 @@ class Artist extends React.Component {
{artist.albums && ( {artist.albums && (
<div className="albums"> <div className="albums">
<h4> <h4>
<div>Albums</div> <div><I18n path="artist.overview.albums" /></div>
<DropdownField <DropdownField
icon="swap_vert" icon="swap_vert"
name="Sort" name="Sort"
value={this.props.sort} value={sort}
valueAsLabel valueAsLabel
options={sort_options} options={sort_options}
selected_icon={this.props.sort ? (this.props.sort_reverse ? 'keyboard_arrow_up' : 'keyboard_arrow_down') : null} selected_icon={sort ? (sort_reverse ? 'keyboard_arrow_up' : 'keyboard_arrow_down') : null}
handleChange={(value) => { this.setSort(value); this.props.uiActions.hideContextMenu(); }} handleChange={this.onChangeSort}
/> />
<DropdownField <DropdownField
icon="filter_list" icon="filter_list"
name="Filter" name="Filter"
value={this.props.filter} value={filter}
valueAsLabel valueAsLabel
options={filter_options} options={filter_options}
handleChange={(value) => { this.setFilter(value); this.props.uiActions.hideContextMenu(); }} handleChange={this.onFilterChange}
/> />
{(this.props.sort || this.props.filter) && ( {(sort || filter) && (
<a className="button button--discrete button--small" onClick={(e) => { this.setFilter(null); this.setSort(null); }}> <a className="button button--discrete button--small" onClick={this.onResetFilters}>
<Icon name="clear" /> <Icon name="clear" />
Reset <I18n path="actions.reset" />
</a> </a>
)} )}
</h4> </h4>
@ -246,7 +300,7 @@ class Artist extends React.Component {
<LazyLoadListener <LazyLoadListener
loadKey={artist.albums_more} loadKey={artist.albums_more}
showLoader={artist.albums_more} showLoader={artist.albums_more}
loadMore={() => this.loadMore()} loadMore={this.loadMore}
/> />
</section> </section>
</div> </div>
@ -255,20 +309,26 @@ class Artist extends React.Component {
); );
} }
renderTracks() { renderTracks = () => {
const {
artist: artistProp,
artists,
tracks,
} = this.props;
const artist = collate( const artist = collate(
this.props.artist, artistProp,
{ {
artists: this.props.artists, artists,
tracks: this.props.tracks, tracks,
}, },
); );
if (!artist.tracks_uris || (artist.tracks_uris && !artist.tracks) || (artist.tracks_uris.length !== artist.tracks.length)) { const is_loading_tracks = (
var is_loading_tracks = true; !artist.tracks_uris
} else { || (artist.tracks_uris && !artist.tracks)
var is_loading_tracks = false; || (artist.tracks_uris.length !== artist.tracks.length)
} );
return ( return (
<div className="body related-artists"> <div className="body related-artists">
@ -280,11 +340,16 @@ class Artist extends React.Component {
); );
} }
renderRelatedArtists() { renderRelatedArtists = () => {
const {
artist: artistProp,
artists,
} = this.props;
const artist = collate( const artist = collate(
this.props.artist, artistProp,
{ {
artists: this.props.artists, artists,
}, },
); );
@ -297,45 +362,48 @@ class Artist extends React.Component {
); );
} }
renderAbout() { renderAbout = () => {
const {
artist: artistProp,
artists,
} = this.props;
const artist = collate( const artist = collate(
this.props.artist, artistProp,
{ {
artists: this.props.artists, artists,
}, },
); );
const thumbnails = []; const thumbnails = artist.images && artist.images.map(
if (artist.images && artist.images.length > 0) { (image) => {
for (const images of artist.images) { if (!image.huge) return null;
// Capture potential 'null' images return (
// TODO: Prevent these from existing in the first place <div className="tile thumbnail-wrapper" key={image.huge}>
if (images.huge) { <Thumbnail size="huge" canZoom fill images={image} />
thumbnails.push( </div>
<div className="tile thumbnail-wrapper" key={images.huge}>
<Thumbnail size="huge" canZoom fill images={images} />
</div>,
); );
} }
} );
}
return ( return (
<div className="body about"> <div className="body about">
<div className="col col--w40 tiles artist-stats"> <div className="col col--w40 tiles artist-stats">
{thumbnails} {thumbnails}
<div className="tile"> <div className="tile">
<span className="content"> <span className="content">
<Icon type="fontawesome" name={sourceIcon(artist.uri)} /> <Icon type="fontawesome" name={sourceIcon(artist.uri)} />
{`${titleCase(uriSource(artist.uri))} artist`} <I18n
path="artist.about.source"
source={titleCase(uriSource(artist.uri))}
/>
</span> </span>
</div> </div>
{artist.followers && ( {artist.followers && (
<div className="tile"> <div className="tile">
<span className="content"> <span className="content">
<Icon type="fontawesome" name="users" /> <Icon type="fontawesome" name="users" />
{`${artist.followers.toLocaleString()} followers`} <I18n path="specs.followers" count={artist.followers.toLocaleString()} />
</span> </span>
</div> </div>
)} )}
@ -343,7 +411,7 @@ class Artist extends React.Component {
<div className="tile"> <div className="tile">
<span className="content"> <span className="content">
<Icon type="fontawesome" name="fire" /> <Icon type="fontawesome" name="fire" />
{`${artist.popularity} % popularity`} <I18n path="specs.popularity" percent={artist.popularity} />
</span> </span>
</div> </div>
)} )}
@ -351,7 +419,7 @@ class Artist extends React.Component {
<div className="tile"> <div className="tile">
<span className="content"> <span className="content">
<Icon type="fontawesome" name="headphones" /> <Icon type="fontawesome" name="headphones" />
{`${artist.listeners.toLocaleString()} listeners`} <I18n path="specs.listeners" count={artist.listeners.toLocaleString()} />
</span> </span>
</div> </div>
)} )}
@ -365,11 +433,17 @@ class Artist extends React.Component {
<p>{artist.biography}</p> <p>{artist.biography}</p>
<br /> <br />
<div className="mid_grey-text"> <div className="mid_grey-text">
{`Published: ${artist.biography_publish_date}`} <I18n path="artist.about.wiki.published" date={artist.biography_publish_date} />
</div> </div>
<div className="mid_grey-text"> <div className="mid_grey-text">
{`Origin: `} <I18n path="artist.about.wiki.origin" />
<a href={artist.biography_link} target="_blank">{ artist.biography_link }</a> <a
href={artist.biography_link}
target="_blank"
rel="noopener noreferrer"
>
{artist.biography_link}
</a>
</div> </div>
</div> </div>
)} )}
@ -379,35 +453,35 @@ class Artist extends React.Component {
); );
} }
render() { render = () => {
const scheme = uriSource(this.props.uri); const {
uri,
load_queue,
artist,
history,
} = this.props;
if (!this.props.artist) { if (!artist) {
if (isLoading(this.props.load_queue, [`spotify_artists/${getFromUri('artistid', this.props.uri)}`, 'lastfm_method=artist.getInfo'])) { if (
isLoading(
load_queue,
[`spotify_artists/${getFromUri('artistid', uri)}`, 'lastfm_method=artist.getInfo'],
)
) {
return <Loader body loading />; return <Loader body loading />;
} }
return ( return (
<ErrorMessage type="not-found" title="Not found"> <ErrorMessage type="not-found" title="Not found">
<p> <p>
{`Could not find artist with URI "${encodeURIComponent(this.props.uri)}"`} <I18n path="errors.uri_not_found" uri={encodeURIComponent(uri)} />
</p> </p>
</ErrorMessage> </ErrorMessage>
); );
} }
if (this.props.artist && this.props.artist.images && this.props.artist.images.length > 0) { const scheme = uriSource(uri);
var image = this.props.artist.images[0].huge; const image = (artist.images && artist.images.length) ? artist.images[0].huge : null;
} else { const is_spotify = (scheme === 'spotify');
var image = null;
}
const is_spotify = (scheme == 'spotify');
if (this.props.artist.tracks_uris && this.props.artist.tracks_uris.length > 0) {
var uris_to_play = this.props.artist.tracks_uris;
} else {
var uris_to_play = this.props.artist.albums_uris;
}
return ( return (
<div className="view artist-view preserve-3d"> <div className="view artist-view preserve-3d">
@ -417,64 +491,69 @@ class Artist extends React.Component {
<div className="liner"> <div className="liner">
<div className="heading"> <div className="heading">
<div className="heading__thumbnail"> <div className="heading__thumbnail">
<Thumbnail size="medium" circle canZoom type="artist" image={image} /> <Thumbnail size="medium" circle canZoom type="artist" image={image} />
</div> </div>
<div className="heading__content"> <div className="heading__content">
<h1>{this.props.artist ? this.props.artist.name : null}</h1> <h1>{this.props.artist ? this.props.artist.name : null}</h1>
<div className="actions"> <div className="actions">
<button className="button button--primary" onClick={(e) => this.props.mopidyActions.playURIs(uris_to_play, this.props.artist.uri)}>Play</button> <button className="button button--primary" onClick={this.onPlay}>
{is_spotify ? <FollowButton uri={this.props.uri} removeText="Remove from library" addText="Add to library" is_following={this.inLibrary()} /> : null} <I18n path="actions.play" />
<ContextMenuTrigger className="white" onTrigger={(e) => this.handleContextMenu(e)} /> </button>
{is_spotify && (
<FollowButton
uri={uri}
is_following={this.inLibrary()}
/>
)}
<ContextMenuTrigger className="white" onTrigger={this.handleContextMenu} />
</div> </div>
</div> </div>
</div> </div>
<div className="sub-views" id="sub-views-menu"> <div className="sub-views" id="sub-views-menu">
<Link <Link
exact exact
history={this.props.history} history={history}
activeClassName="sub-views__option--active" activeClassName="sub-views__option--active"
className="sub-views__option" className="sub-views__option"
to={`/artist/${encodeURIComponent(this.props.uri)}`} to={`/artist/${encodeURIComponent(uri)}`}
scrollTo="#sub-views-menu" scrollTo="#sub-views-menu"
> >
<h4>Overview</h4> <h4><I18n path="artist.overview.title" /></h4>
</Link> </Link>
{this.props.artist.tracks_uris && this.props.artist.tracks_uris.length > 10 && ( {artist.tracks_uris && artist.tracks_uris.length > 10 && (
<Link <Link
exact exact
history={this.props.history} history={history}
activeClassName="sub-views__option--active" activeClassName="sub-views__option--active"
className="sub-views__option" className="sub-views__option"
to={`/artist/${encodeURIComponent(this.props.uri)}/tracks`} to={`/artist/${encodeURIComponent(uri)}/tracks`}
scrollTo="#sub-views-menu" scrollTo="#sub-views-menu"
> >
<h4>Tracks</h4> <h4><I18n path="artist.tracks.title" /></h4>
</Link> </Link>
)} )}
{this.props.artist.related_artists_uris && ( {artist.related_artists_uris && (
<Link <Link
exact exact
history={this.props.history} history={history}
activeClassName="sub-views__option--active" activeClassName="sub-views__option--active"
className="sub-views__option" className="sub-views__option"
to={`/artist/${encodeURIComponent(this.props.uri)}/related-artists`} to={`/artist/${encodeURIComponent(uri)}/related-artists`}
scrollTo="#sub-views-menu" scrollTo="#sub-views-menu"
> >
<h4>Related artists</h4> <h4><I18n path="artist.related_artists.title" /></h4>
</Link> </Link>
)} )}
<Link <Link
exact exact
history={this.props.history} history={history}
activeClassName="sub-views__option--active" activeClassName="sub-views__option--active"
className="sub-views__option" className="sub-views__option"
to={`/artist/${encodeURIComponent(this.props.uri)}/about`} to={`/artist/${encodeURIComponent(uri)}/about`}
scrollTo="#sub-views-menu" scrollTo="#sub-views-menu"
> >
<h4>About</h4> <h4><I18n path="artist.about.title" /></h4>
</Link> </Link>
</div> </div>
</div> </div>

View File

@ -9,6 +9,7 @@ import * as pusherActions from '../services/pusher/actions';
import * as mopidyActions from '../services/mopidy/actions'; import * as mopidyActions from '../services/mopidy/actions';
import * as spotifyActions from '../services/spotify/actions'; import * as spotifyActions from '../services/spotify/actions';
import * as snapcastActions from '../services/snapcast/actions'; import * as snapcastActions from '../services/snapcast/actions';
import { I18n, i18n } from '../locale';
class Debug extends React.Component { class Debug extends React.Component {
constructor(props) { constructor(props) {
@ -24,152 +25,196 @@ class Debug extends React.Component {
} }
componentDidMount() { componentDidMount() {
this.props.uiActions.setWindowTitle('Debug'); const { uiActions: { setWindowTitle } } = this.props;
setWindowTitle(i18n('debug.title'));
} }
callMopidy(e) { onBack = () => {
const { history } = this.props;
history.push('/settings');
}
callMopidy = (e) => {
const { mopidyActions: { debug } } = this.props;
const { mopidy_call, mopidy_data } = this.state;
e.preventDefault(); e.preventDefault();
this.props.mopidyActions.debug(this.state.mopidy_call, JSON.parse(this.state.mopidy_data)); debug(mopidy_call, JSON.parse(mopidy_data));
} }
callPusher(e) { callPusher = (e) => {
const { pusherActions: { debug } } = this.props;
const { pusher_data } = this.state;
e.preventDefault(); e.preventDefault();
this.props.pusherActions.debug(JSON.parse(this.state.pusher_data)); debug(JSON.parse(pusher_data));
} }
callSnapcast(e) { callSnapcast = (e) => {
const { snapcastActions: { debug } } = this.props;
const { snapcast_data } = this.state;
e.preventDefault(); e.preventDefault();
this.props.snapcastActions.debug(JSON.parse(this.state.snapcast_data)); debug(JSON.parse(snapcast_data));
} }
toggleTestMode(e) { toggleTestMode = (e) => {
const { uiActions: { set }, test_mode } = this.props;
this.setState({ toggling_test_mode: true }); this.setState({ toggling_test_mode: true });
this.props.uiActions.set({ test_mode: !this.props.test_mode }); set({ test_mode: !test_mode });
// Wait a second to allow state to update, and then refresh // Wait a second to allow state to update, and then refresh
setTimeout(location.reload(), 1000); setTimeout(location.reload(), 1000);
} }
render() { render = () => {
const {
uiActions,
spotifyActions,
history,
test_mode,
debug_response,
debug_info,
log_actions,
log_mopidy,
log_pusher,
log_snapcast,
access_token,
} = this.props;
const {
toggling_test_mode,
mopidy_call,
mopidy_data,
pusher_data,
snapcast_data,
} = this.state;
const options = ( const options = (
<a className="button button--discrete button--no-hover" onClick={(e) => this.props.history.push('/settings')}> <a className="button button--discrete button--no-hover" onClick={this.onBack}>
<Icon name="keyboard_backspace" /> <Icon name="keyboard_backspace" />
Back <I18n path="actions.back" />
</a> </a>
); );
return ( return (
<div className="view debugger-view"> <div className="view debugger-view">
<Header options={options} uiActions={this.props.uiActions}> <Header options={options} uiActions={uiActions}>
<Icon name="settings" type="material" /> <Icon name="settings" type="material" />
Debug <I18n path="debug.title" />
</Header> </Header>
<div className="content-wrapper"> <div className="content-wrapper">
<h4 className="underline">User interface</h4> <h4 className="underline"><I18n path="debug.ui.title" /></h4>
<form> <form>
<div className="field checkbox"> <div className="field checkbox">
<div className="name">Test mode</div> <div className="name">Test mode</div>
<div className="input"> <div className="input">
{this.state.toggling_test_mode ? <span className="button button--working">Applying...</span> : (this.props.test_mode ? <span className="button button--destructive" onClick={(e) => this.toggleTestMode(e)}>Disable</span> : <span className="button button--primary" onClick={(e) => this.toggleTestMode(e)}>Enable</span>)} {toggling_test_mode && (
<span className="button button--working">Applying...</span>
)}
{!toggling_test_mode && (
<span
className={`button button--${test_mode ? 'destructive' : 'default'}`}
onClick={this.toggleTestMode}
>
<I18n path={`actions.${test_mode ? 'disable' : 'enable'}`} />
</span>
)}
</div> </div>
</div> </div>
<div className="field checkbox"> <div className="field checkbox">
<div className="name">Debug</div> <div className="name"><I18n path="debug.debug.title" /></div>
<div className="input"> <div className="input">
<label> <label>
<input <input
type="checkbox" type="checkbox"
name="debug_info" name="debug_info"
checked={this.props.debug_info} checked={debug_info}
onChange={(e) => this.props.uiActions.set({ debug_info: !this.props.debug_info })} onChange={() => uiActions.set({ debug_info: !debug_info })}
/> />
<span className="label">Show debug overlay</span> <span className="label"><I18n path="debug.debug.overlay" /></span>
</label> </label>
</div> </div>
</div> </div>
<div className="field checkbox"> <div className="field checkbox">
<div className="name">Logging</div> <div className="name"><I18n path="debug.logging.title" /></div>
<div className="input"> <div className="input">
<label> <label>
<input <input
type="checkbox" type="checkbox"
name="log_actions" name="log_actions"
checked={this.props.log_actions} checked={log_actions}
onChange={(e) => this.props.uiActions.set({ log_actions: !this.props.log_actions })} onChange={() => uiActions.set({ log_actions: !log_actions })}
/> />
<span className="label">Log actions</span> <span className="label"><I18n path="debug.logging.actions" /></span>
</label> </label>
<label> <label>
<input <input
type="checkbox" type="checkbox"
name="log_mopidy" name="log_mopidy"
checked={this.props.log_mopidy} checked={log_mopidy}
onChange={(e) => this.props.uiActions.set({ log_mopidy: !this.props.log_mopidy })} onChange={() => uiActions.set({ log_mopidy: !log_mopidy })}
/> />
<span className="label">Log Mopidy</span> <span className="label"><I18n path="debug.logging.mopidy" /></span>
</label> </label>
<label> <label>
<input <input
type="checkbox" type="checkbox"
name="log_pusher" name="log_pusher"
checked={this.props.log_pusher} checked={log_pusher}
onChange={(e) => this.props.uiActions.set({ log_pusher: !this.props.log_pusher })} onChange={() => uiActions.set({ log_pusher: !log_pusher })}
/> />
<span className="label">Log Pusher</span> <span className="label"><I18n path="debug.logging.pusher" /></span>
</label> </label>
<label> <label>
<input <input
type="checkbox" type="checkbox"
name="log_pusher" name="log_pusher"
checked={this.props.log_snapcast} checked={log_snapcast}
onChange={(e) => this.props.uiActions.set({ log_snapcast: !this.props.log_snapcast })} onChange={() => uiActions.set({ log_snapcast: !log_snapcast })}
/> />
<span className="label">Log Snapcast</span> <span className="label"><I18n path="debug.logging.snapcast" /></span>
</label> </label>
</div> </div>
</div> </div>
<div className="field"> <div className="field">
<div className="name" /> <div className="name" />
<div className="input"> <div className="input">
<a className="button button--default" onClick={(e) => this.props.uiActions.createNotification({ content: 'Test notification' })}>Create notification</a> <a className="button button--default" onClick={(e) => this.props.uiActions.createNotification({ content: 'Test notification' })}><I18n path="debug.create_notification" /></a>
<a className="button button--default" onClick={(e) => this.props.uiActions.startProcess('test_process', "Test process", {remaining: 68, total: 100})}>Create process notification</a> <a className="button button--default" onClick={(e) => this.props.uiActions.startProcess('test_process', "Test process", {remaining: 68, total: 100})}><I18n path="debug.create_process" /></a>
<a className="button button--default" onClick={(e) => this.props.pusherActions.request('test')}>Run test process</a> <a className="button button--default" onClick={(e) => this.props.pusherActions.request('test')}><I18n path="debug.run_test" /></a>
</div> </div>
</div> </div>
</form> </form>
<h4 className="underline">Spotify</h4> <h4 className="underline">Spotify</h4>
<label className="field"> <label className="field">
<div className="name">Access token</div> <div className="name"><I18n path="debug.access_token" /></div>
<div className="input"> <div className="input">
<input <input
type="text" type="text"
onChange={(e) => this.props.spotifyActions.authorizationGranted({ access_token: e.target.value })} onChange={(e) => spotifyActions.authorizationGranted({ access_token: e.target.value })}
value={this.state.access_token} value={access_token}
/> />
</div> </div>
</label> </label>
<h4 className="underline">Mopidy</h4> <h4 className="underline"><I18n path="services.mopidy.title" /></h4>
<form onSubmit={(e) => this.callMopidy(e)}> <form onSubmit={this.callMopidy}>
<label className="field"> <label className="field">
<div className="name">Call</div> <div className="name"><I18n path="debug.call" /></div>
<div className="input"> <div className="input">
<input <input
type="text" type="text"
onChange={(e) => this.setState({ mopidy_call: e.target.value })} onChange={(e) => this.setState({ mopidy_call: e.target.value })}
value={this.state.mopidy_call} value={mopidy_call}
/> />
</div> </div>
</label> </label>
<label className="field"> <label className="field">
<div className="name">Data</div> <div className="name"><I18n path="debug.data" /></div>
<div className="input"> <div className="input">
<textarea <textarea
onChange={(e) => this.setState({ mopidy_data: e.target.value })} onChange={(e) => this.setState({ mopidy_data: e.target.value })}
value={this.state.mopidy_data} value={mopidy_data}
/> />
</div> </div>
</label> </label>
@ -181,10 +226,10 @@ Back
</div> </div>
</form> </form>
<h4 className="underline">Pusher</h4> <h4 className="underline"><I18n path="services.pusher.title" /></h4>
<form onSubmit={(e) => this.callPusher(e)}> <form onSubmit={this.callPusher}>
<label className="field"> <label className="field">
<div className="name">Examples</div> <div className="name"><I18n path="debug.examples" /></div>
<div className="input"> <div className="input">
<select onChange={(e) => this.setState({ pusher_data: e.target.value })}> <select onChange={(e) => this.setState({ pusher_data: e.target.value })}>
<option value='{"method":"get_config"}'>Get config</option> <option value='{"method":"get_config"}'>Get config</option>
@ -202,44 +247,44 @@ Back
</div> </div>
</label> </label>
<label className="field"> <label className="field">
<div className="name">Data</div> <div className="name"><I18n path="debug.data" /></div>
<div className="input"> <div className="input">
<textarea <textarea
onChange={(e) => this.setState({ pusher_data: e.target.value })} onChange={(e) => this.setState({ pusher_data: e.target.value })}
value={this.state.pusher_data} value={pusher_data}
/> />
</div> </div>
</label> </label>
<div className="field"> <div className="field">
<div className="name" /> <div className="name" />
<div className="input"> <div className="input">
<button type="submit" className="button button--default">Send</button> <button type="submit" className="button button--default"><I18n path="actions.send" /></button>
</div> </div>
</div> </div>
</form> </form>
<h4 className="underline">Snapcast</h4> <h4 className="underline"><I18n path="services.snapcast.title" /></h4>
<form onSubmit={(e) => this.callSnapcast(e)}> <form onSubmit={(e) => this.callSnapcast(e)}>
<label className="field"> <label className="field">
<div className="name">Data</div> <div className="name"><I18n path="debug.data" /></div>
<div className="input"> <div className="input">
<textarea <textarea
onChange={(e) => this.setState({ snapcast_data: e.target.value })} onChange={(e) => this.setState({ snapcast_data: e.target.value })}
value={this.state.snapcast_data} value={snapcast_data}
/> />
</div> </div>
</label> </label>
<div className="field"> <div className="field">
<div className="name" /> <div className="name" />
<div className="input"> <div className="input">
<button type="submit" className="button button--default">Send</button> <button type="submit" className="button button--default"><I18n path="actions.send" /></button>
</div> </div>
</div> </div>
</form> </form>
<h4 className="underline">Response</h4> <h4 className="underline"><I18n path="debug.response" /></h4>
<pre> <pre>
{ this.props.debug_response ? JSON.stringify(this.props.debug_response, null, 2) : null } {debug_response && JSON.stringify(debug_response, null, 2)}
</pre> </pre>
</div> </div>
@ -248,7 +293,7 @@ Back
} }
} }
const mapStateToProps = (state, ownProps) => ({ const mapStateToProps = (state) => ({
connection_id: state.pusher.connection_id, connection_id: state.pusher.connection_id,
access_token: (state.spotify.access_token ? state.spotify.access_token : ''), access_token: (state.spotify.access_token ? state.spotify.access_token : ''),
log_actions: (state.ui.log_actions ? state.ui.log_actions : false), log_actions: (state.ui.log_actions ? state.ui.log_actions : false),

View File

@ -9,7 +9,7 @@ import TrackList from '../components/TrackList';
import Thumbnail from '../components/Thumbnail'; import Thumbnail from '../components/Thumbnail';
import Parallax from '../components/Parallax'; import Parallax from '../components/Parallax';
import NiceNumber from '../components/NiceNumber'; import NiceNumber from '../components/NiceNumber';
import Dater from '../components/Dater'; import { Dater } from '../components/Dater';
import LazyLoadListener from '../components/LazyLoadListener'; import LazyLoadListener from '../components/LazyLoadListener';
import FollowButton from '../components/Fields/FollowButton'; import FollowButton from '../components/Fields/FollowButton';
import Loader from '../components/Loader'; import Loader from '../components/Loader';

View File

@ -5,7 +5,7 @@ import Link from '../components/Link';
import Icon from '../components/Icon'; import Icon from '../components/Icon';
import Parallax from '../components/Parallax'; import Parallax from '../components/Parallax';
import TrackList from '../components/TrackList'; import TrackList from '../components/TrackList';
import Dater from '../components/Dater'; import { Dater } from '../components/Dater';
import LinksSentence from '../components/LinksSentence'; import LinksSentence from '../components/LinksSentence';
import Thumbnail from '../components/Thumbnail'; import Thumbnail from '../components/Thumbnail';
import Header from '../components/Header'; import Header from '../components/Header';
@ -20,7 +20,7 @@ import {
getFromUri, getFromUri,
uriType, uriType,
} from '../util/helpers'; } from '../util/helpers';
import { content, Content } from '../locale'; import { i18n, I18n } from '../locale';
class Queue extends React.Component { class Queue extends React.Component {
constructor(props) { constructor(props) {
@ -40,7 +40,7 @@ class Queue extends React.Component {
if (limit) { if (limit) {
this.setState({ limit }); this.setState({ limit });
} }
setWindowTitle(content('now_playing.title')); setWindowTitle(i18n('now_playing.title'));
} }
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
@ -77,7 +77,7 @@ class Queue extends React.Component {
} }
} }
loadMore() { loadMore = () => {
const { limit, per_page } = this.state; const { limit, per_page } = this.state;
const { location, history } = this.props; const { location, history } = this.props;
const new_limit = limit + per_page; const new_limit = limit + per_page;
@ -92,7 +92,7 @@ class Queue extends React.Component {
history.replace({ state }); history.replace({ state });
} }
removeTracks(track_indexes) { removeTracks = (track_indexes) => {
const { queue_tracks, mopidyActions } = this.props; const { queue_tracks, mopidyActions } = this.props;
const tlids = []; const tlids = [];
for (let i = 0; i < track_indexes.length; i++) { for (let i = 0; i < track_indexes.length; i++) {
@ -107,21 +107,23 @@ class Queue extends React.Component {
} }
} }
playTrack(track) { playTrack = (track) => {
this.props.mopidyActions.changeTrack(track.tlid); const { mopidyActions: { changeTrack } } = this.props;
changeTrack(track.tlid);
} }
playTracks(tracks) { playTracks = (tracks) => {
this.props.mopidyActions.changeTrack(tracks[0].tlid); const { mopidyActions: { changeTrack } } = this.props;
changeTrack(tracks[0].tlid);
} }
reorderTracks(indexes, index) { reorderTracks = (indexes, index) => {
this.props.mopidyActions.reorderTracklist(indexes, index); const { mopidyActions: { reorderTracklist } } = this.props;
reorderTracklist(indexes, index);
} }
renderArtwork(image) { renderArtwork = (image) => {
const { current_track } = this.props; const { current_track: { album: { uri } = {} } = {} } = this.props;
const uri = (current_track && current_track.album && current_track.album.uri) ? current_track.album.uri : null;
if (!image) { if (!image) {
return ( return (
@ -145,7 +147,7 @@ class Queue extends React.Component {
); );
} }
renderAddedFrom() { renderAddedFrom = () => {
const { added_from_uri } = this.props; const { added_from_uri } = this.props;
if (!added_from_uri) return null; if (!added_from_uri) return null;
@ -203,7 +205,7 @@ class Queue extends React.Component {
<LinksSentence items={items} /> <LinksSentence items={items} />
{uri_type === 'radio' && ( {uri_type === 'radio' && (
<span className="flag flag--blue"> <span className="flag flag--blue">
{content('now_playing.current_track.radio')} {i18n('now_playing.current_track.radio')}
</span> </span>
)} )}
</div> </div>
@ -211,7 +213,7 @@ class Queue extends React.Component {
); );
} }
render() { render = () => {
const { current_track, queue_tracks, theme } = this.props; const { current_track, queue_tracks, theme } = this.props;
const { limit } = this.state; const { limit } = this.state;
const total_queue_tracks = queue_tracks.length; const total_queue_tracks = queue_tracks.length;
@ -229,16 +231,16 @@ class Queue extends React.Component {
{this.props.spotify_enabled && ( {this.props.spotify_enabled && (
<Link className="button button--no-hover" to="/queue/radio"> <Link className="button button--no-hover" to="/queue/radio">
<Icon name="radio" /> <Icon name="radio" />
{content('now_playing.context_actions.radio')} <I18n path="now_playing.context_actions.radio" />
</Link> </Link>
)} )}
<Link className="button button--no-hover" to="/queue/history"> <Link className="button button--no-hover" to="/queue/history">
<Icon name="history" /> <Icon name="history" />
{content('now_playing.context_actions.history')} <I18n path="now_playing.context_actions.history" />
</Link> </Link>
<Link className="button button--no-hover" to="/queue/add-uri"> <Link className="button button--no-hover" to="/queue/add-uri">
<Icon name="playlist_add" /> <Icon name="playlist_add" />
{content('now_playing.context_actions.add_uri')} <I18n path="now_playing.context_actions.add_uri" />
</Link> </Link>
</span> </span>
); );
@ -247,7 +249,7 @@ class Queue extends React.Component {
<div className="view queue-view preserve-3d"> <div className="view queue-view preserve-3d">
<Header options={options} uiActions={this.props.uiActions}> <Header options={options} uiActions={this.props.uiActions}>
<Icon name="play_arrow" type="material" /> <Icon name="play_arrow" type="material" />
{content('now_playing.title')} <I18n path="now_playing.title" />
</Header> </Header>
{theme === 'dark' && <Parallax image={current_track_image} blur />} {theme === 'dark' && <Parallax image={current_track_image} blur />}
<div className="content-wrapper"> <div className="content-wrapper">
@ -283,7 +285,7 @@ class Queue extends React.Component {
<li> <li>
<a onClick={this.props.mopidyActions.shuffleTracklist}> <a onClick={this.props.mopidyActions.shuffleTracklist}>
<Icon name="shuffle" /> <Icon name="shuffle" />
{content('now_playing.current_track.shuffle')} <I18n path="now_playing.current_track.shuffle" />
</a> </a>
</li> </li>
)} )}
@ -291,7 +293,7 @@ class Queue extends React.Component {
<li> <li>
<a onClick={this.props.mopidyActions.clearTracklist}> <a onClick={this.props.mopidyActions.clearTracklist}>
<Icon name="delete_sweep" /> <Icon name="delete_sweep" />
{content('now_playing.current_track.clear')} <I18n path="now_playing.current_track.clear" />
</a> </a>
</li> </li>
)} )}
@ -307,10 +309,10 @@ class Queue extends React.Component {
track_context="queue" track_context="queue"
className="queue-track-list" className="queue-track-list"
tracks={tracks} tracks={tracks}
removeTracks={(track_indexes) => this.removeTracks(track_indexes)} removeTracks={this.removeTracks}
playTracks={(tracks) => this.playTracks(tracks)} playTracks={this.playTracks}
playTrack={(track) => this.playTrack(track)} playTrack={this.playTrack}
reorderTracks={(indexes, index) => this.reorderTracks(indexes, index)} reorderTracks={this.reorderTracks}
/> />
</section> </section>
@ -321,7 +323,7 @@ class Queue extends React.Component {
: total_queue_tracks : total_queue_tracks
} }
showLoader={limit < total_queue_tracks} showLoader={limit < total_queue_tracks}
loadMore={() => this.loadMore()} loadMore={this.loadMore}
/> />
</div> </div>
</div> </div>

View File

@ -20,7 +20,7 @@ import * as mopidyActions from '../services/mopidy/actions';
import * as lastfmActions from '../services/lastfm/actions'; import * as lastfmActions from '../services/lastfm/actions';
import * as spotifyActions from '../services/spotify/actions'; import * as spotifyActions from '../services/spotify/actions';
import { isHosted } from '../util/helpers'; import { isHosted } from '../util/helpers';
import { content } from '../locale'; import { i18n } from '../locale';
class Settings extends React.Component { class Settings extends React.Component {
constructor(props) { constructor(props) {
@ -37,7 +37,7 @@ class Settings extends React.Component {
componentDidMount() { componentDidMount() {
const { uiActions: { setWindowTitle } } = this.props; const { uiActions: { setWindowTitle } } = this.props;
setWindowTitle(content('settings.title')); setWindowTitle(i18n('settings.title'));
} }
componentDidUpdate = () => { componentDidUpdate = () => {

View File

@ -8,7 +8,7 @@ import Header from '../components/Header';
import Thumbnail from '../components/Thumbnail'; import Thumbnail from '../components/Thumbnail';
import LinksSentence from '../components/LinksSentence'; import LinksSentence from '../components/LinksSentence';
import LastfmLoveButton from '../components/Fields/LastfmLoveButton'; import LastfmLoveButton from '../components/Fields/LastfmLoveButton';
import Dater from '../components/Dater'; import { Dater } from '../components/Dater';
import SelectField from '../components/Fields/SelectField'; import SelectField from '../components/Fields/SelectField';
import ContextMenuTrigger from '../components/ContextMenuTrigger'; import ContextMenuTrigger from '../components/ContextMenuTrigger';
import Icon from '../components/Icon'; import Icon from '../components/Icon';