Upgrading links sentence; multi-seed radio

This commit is contained in:
James Barnsley
2019-10-05 09:10:34 +13:00
parent d1d2ee5ca3
commit 96d68c0ae0
29 changed files with 502 additions and 552 deletions

View File

@ -1,43 +0,0 @@
import React, { memo } from 'react';
import URILink from './URILink';
export default memo((props) => {
if (!props.artists) {
return <span className={props.className ? `${props.className} artist-sentence` : 'artist-sentence'}>-</span>;
}
return (
<span className={props.className ? `${props.className} artist-sentence` : 'artist-sentence'}>
{
props.artists.map((artist, index) => {
if (!artist) {
return <span>-</span>;
}
let separator = null;
if (index == props.artists.length - 2) {
separator = ' and ';
} else if (index < props.artists.length - 2) {
separator = ', ';
}
if (!artist.name) {
var content = <span>-</span>;
} else if (!artist.uri || props.nolinks) {
var content = <span>{ artist.name }</span>;
} else {
var content = <URILink className="artist" type="artist" uri={artist.uri}>{ artist.name }</URILink>;
}
return (
<span key={`index_${artist.uri}`}>
{ content }
{ separator }
</span>
);
})
}
</span>
);
});

View File

@ -43,24 +43,21 @@ const durationTime = (milliseconds = null) => {
* @return string (eg 2+ hours)
* */
const durationSentence = (milliseconds = null) => {
if (!milliseconds) return null;
const string = '';
let total_hours; let total_minutes; let total_seconds; let minutes; let
seconds;
if (milliseconds === null) return null;
// get total values for each
total_seconds = Math.floor(milliseconds / 1000);
total_minutes = Math.floor(milliseconds / (1000 * 60));
total_hours = Math.floor(milliseconds / (1000 * 60 * 60));
const totalSeconds = Math.floor(milliseconds / 1000);
const totalMinutes = Math.floor(milliseconds / (1000 * 60));
const totalHours = Math.floor(milliseconds / (1000 * 60 * 60));
if (total_hours > 1) return `${total_hours}+ hrs`;
if (total_minutes > 1) return `${total_minutes} mins`;
if (total_seconds) return `${total_seconds} sec`;
if (totalHours > 1) return `${totalHours}+ hrs`;
if (totalMinutes > 1) return `${totalMinutes} mins`;
if (totalSeconds) return `${totalSeconds} sec`;
return '0 mins';
};
export default memo((props) => {
if (!props.data) {
if (props.data === undefined) {
return null;
}

View File

@ -5,13 +5,13 @@ import { bindActionCreators } from 'redux';
import Link from '../Link';
import ArtistSentence from '../ArtistSentence';
import LinksSentence from '../LinksSentence';
import * as helpers from '../../helpers';
import * as coreActions from '../../services/core/actions';
import * as uiActions from '../../services/ui/actions';
import * as spotifyActions from '../../services/spotify/actions';
class AddSeedField extends React.Component {
class AddSeedField extends React.Component {
constructor(props) {
super(props);
@ -71,7 +71,7 @@ class AddSeedField extends React.Component {
} if (this.props.results[this.id] === undefined) {
return null;
}
return this.props.results[this.id];
return this.props.results[this.id];
}
renderResults(type) {
@ -88,11 +88,11 @@ class AddSeedField extends React.Component {
items.map((item) => (
<div className="result" key={item.uri} onClick={(e) => this.handleSelect(e, item)}>
{item.name}
{type == 'tracks' ? (
<span className="mid_grey-text">
{' '}
<ArtistSentence artists={item.artists} nolinks />
</span>
{type == 'tracks' ? (
<span className="mid_grey-text">
{' '}
<LinksSentence items={item.artists} nolinks />
</span>
) : null}
</div>
))
@ -113,7 +113,7 @@ class AddSeedField extends React.Component {
type="text"
value={this.state.value}
onChange={(e) => this.handleChange(e, e.target.value)}
placeholder={this.props.placeholder ? this.props.placeholder : 'Start typing...'}
placeholder={this.props.placeholder ? this.props.placeholder : 'Start typing...'}
/>
</div>
<div className="results">
@ -137,4 +137,4 @@ const mapDispatchToProps = (dispatch) => ({
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(AddSeedField);
export default connect(mapStateToProps, mapDispatchToProps)(AddSeedField);

View File

@ -5,13 +5,13 @@ import { bindActionCreators } from 'redux';
import Link from './Link';
import ArtistSentence from '../ArtistSentence';
import LinksSentence from '../LinksSentence';
import * as helpers from '../../helpers';
import * as coreActions from '../../services/core/actions';
import * as uiActions from '../../services/ui/actions';
import * as spotifyActions from '../../services/spotify/actions';
class AddSeedField extends React.Component {
class AddSeedField extends React.Component {
constructor(props) {
super(props);
@ -65,7 +65,7 @@ class AddSeedField extends React.Component {
this.props.spotifyActions.clearAutocompleteResults(this.id);
// Add our selected item to our global index
switch (helpers.uriType(item.uri)) {
switch (helpers.uriType(item.uri)) {
case 'artist':
this.props.coreActions.artistsLoaded(item);
break;
@ -82,7 +82,7 @@ class AddSeedField extends React.Component {
} if (typeof (this.props.results[this.id]) === 'undefined') {
return null;
}
return this.props.results[this.id];
return this.props.results[this.id];
}
renderResults(type) {
@ -99,11 +99,11 @@ class AddSeedField extends React.Component {
items.map((item) => (
<div className="result" key={item.uri} onClick={(e) => this.handleSelect(e, item)}>
{item.name}
{type == 'tracks' ? (
<span className="mid_grey-text">
{' '}
<ArtistSentence artists={item.artists} nolinks />
</span>
{type == 'tracks' ? (
<span className="mid_grey-text">
{' '}
<LinksSentence items={item.artists} nolinks />
</span>
) : null}
</div>
))
@ -124,7 +124,7 @@ class AddSeedField extends React.Component {
type="text"
value={this.state.value}
onChange={(e) => this.handleChange(e, e.target.value)}
placeholder={this.props.placeholder ? this.props.placeholder : 'Start typing...'}
placeholder={this.props.placeholder ? this.props.placeholder : 'Start typing...'}
/>
</div>
<div className="results">
@ -148,4 +148,4 @@ const mapDispatchToProps = (dispatch) => ({
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(AddSeedField);
export default connect(mapStateToProps, mapDispatchToProps)(AddSeedField);

View File

@ -4,7 +4,7 @@ import Sortable from 'react-sortablejs';
import Icon from '../Icon';
import * as helpers from '../../helpers';
export default class SourcesPriority extends React.Component {
export default class SourcesPriority extends React.Component {
constructor(props) {
super(props);
}
@ -40,14 +40,14 @@ export default class SourcesPriority extends React.Component {
className={className}
onChange={(order, sortable, e) => {
this.handleSort(order);
}}
}}
>
{
ordered_schemes.map((scheme) => {
const name = helpers.titleCase(scheme.replace(':', '').replace('+', ' '));
return (
<span className="source flag grey" key={scheme} data-id={scheme}>
<span className="source flag flag--grey" key={scheme} data-id={scheme}>
<Icon name="drag_indicator" />
{name}
</span>
@ -57,4 +57,4 @@ export default class SourcesPriority extends React.Component {
</Sortable>
);
}
}
}

View File

@ -5,7 +5,7 @@ import * as helpers from '../helpers';
import Link from './Link';
import Icon from './Icon';
import Thumbnail from './Thumbnail';
import ArtistSentence from './ArtistSentence';
import LinksSentence from './LinksSentence';
export default class GridItem extends React.Component {
componentDidMount() {
@ -75,7 +75,7 @@ tracks
case 'album':
return (
<span className="grid__item__secondary__content">
{item.artists !== undefined ? <ArtistSentence nolinks artists={item.artists} /> : null}
{item.artists !== undefined ? <LinksSentence nolinks items={item.artists} /> : null}
</span>
);
break;
@ -83,7 +83,7 @@ tracks
default:
return (
<span className="grid__item__secondary__content">
{ item.artists !== undefined ? <ArtistSentence nolinks artists={item.artists} /> : null }
{ item.artists !== undefined ? <LinksSentence nolinks items={item.artists} /> : null }
{ item.followers !== undefined ? `${item.followers.toLocaleString()} followers` : null }
</span>
);

View File

@ -5,12 +5,12 @@ import { createStore, bindActionCreators } from 'redux';
import Link from './Link';
import ArtistSentence from './ArtistSentence';
import LinksSentence from './LinksSentence';
import Thumbnail from './Thumbnail';
import * as uiActions from '../services/ui/actions';
class GridSlider extends React.Component {
class GridSlider extends React.Component {
constructor(props) {
super(props);
@ -47,7 +47,7 @@ class GridSlider extends React.Component {
}
render() {
if (this.props.tracks) {
if (this.props.tracks) {
let className = 'grid-slider-wrapper';
if (this.props.className) className += ` ${this.props.className}`;
@ -69,16 +69,16 @@ class GridSlider extends React.Component {
(track, index) => {
const album = { ...track.album, artists: track.artists };
return (
<div
<div
className="grid-item"
key={index}
onClick={(e) => this.handleClick(e, `/album/${album.uri}`)}
onContextMenu={(e) => this.handleContextMenu(e, album)}
onContextMenu={(e) => this.handleContextMenu(e, album)}
>
<Thumbnail size="medium" images={album.images} />
<div className="name">{ album.name }</div>
<div className="secondary">
{ album.artists ? <ArtistSentence artists={album.artists} /> : <span>-</span> }
{ album.artists ? <LinksSentence items={album.artists} /> : <span>-</span> }
</div>
</div>
);

View File

@ -0,0 +1,43 @@
import React, { memo } from 'react';
import URILink from './URILink';
export default memo((props) => {
if (!props.items) {
return <span className={props.className ? `${props.className} links-sentence` : 'links-sentence'}>-</span>;
}
return (
<span className={props.className ? `${props.className} links-sentence` : 'links-sentence'}>
{
props.items.map((item, index) => {
if (!item) {
return <span>-</span>;
}
let separator = null;
if (index == props.items.length - 2) {
separator = ' and ';
} else if (index < props.items.length - 2) {
separator = ', ';
}
if (!item.name) {
var content = <span>-</span>;
} else if (!item.uri || props.nolinks) {
var content = <span>{ item.name }</span>;
} else {
var content = <URILink className="links-sentence__item links-sentence__item--link" uri={item.uri}>{ item.name }</URILink>;
}
return (
<span key={`index_${item.uri}`}>
{ content }
{ separator }
</span>
);
})
}
</span>
);
});

View File

@ -1,7 +1,7 @@
import React from 'react';
import ArtistSentence from './ArtistSentence';
import LinksSentence from './LinksSentence';
import Dater from './Dater';
import URILink from './URILink';
import ContextMenuTrigger from './ContextMenuTrigger';
@ -11,7 +11,7 @@ import Popularity from './Popularity';
import * as helpers from '../helpers';
export default class ListItem extends React.Component {
export default class ListItem extends React.Component {
constructor(props) {
super(props);
}
@ -23,7 +23,7 @@ export default class ListItem extends React.Component {
// If the item that has just been mounted doesn't have images,
// try fetching them from LastFM
if (!item.images) {
switch (helpers.uriType(item.uri)) {
switch (helpers.uriType(item.uri)) {
case 'artist':
if (discogsActions) {
discogsActions.getArtistImages(item.uri, item);
@ -39,7 +39,7 @@ export default class ListItem extends React.Component {
}
}
handleClick(e) {
handleClick(e) {
// make sure we haven't clicked a nested link (ie Artist name)
if (e.target.tagName.toLowerCase() !== 'a') {
e.preventDefault();
@ -48,7 +48,7 @@ export default class ListItem extends React.Component {
}
}
handleMouseDown(e) {
handleMouseDown(e) {
// make sure we haven't clicked a nested link (ie Artist name)
if (e.target.tagName.toLowerCase() !== 'a') {
e.preventDefault();
@ -74,40 +74,40 @@ export default class ListItem extends React.Component {
} if (typeof (value[key[i]]) === 'string' && value[key[i]].replace(' ', '') == '') {
return null;
}
value = value[key[i]];
value = value[key[i]];
}
if (key_string === 'tracks_total' || key_string === 'tracks_uris.length') {
return (
<span>
{value}
{' '}
tracks
</span>
);
if (key_string === 'tracks_total' || key_string === 'tracks_uris.length') {
return (
<span>
{value}
{' '}
tracks
</span>
);
}
if (key_string === 'followers') {
return (
<span>
{value.toLocaleString()}
{' '}
followers
</span>
);
if (key_string === 'followers') {
return (
<span>
{value.toLocaleString()}
{' '}
followers
</span>
);
}
if (key_string === 'added_at') {
return (
<span>
Added
<Dater type="ago" data={value} />
{' '}
ago
</span>
);
if (key_string === 'added_at') {
return (
<span>
Added
<Dater type="ago" data={value} />
{' '}
ago
</span>
);
}
if (key_string === 'owner') return <URILink type="user" uri={value.uri}>{value.id}</URILink>;
if (key_string === 'popularity') return <Popularity full popularity={value} />;
if (key_string === 'artists') return <ArtistSentence artists={value} />;
if (key_string === 'artists') return <LinksSentence items={value} />;
if (value === true) return <Icon name="check" />;
if (typeof (value) === 'number') return <span>{value.toLocaleString()}</span>;
return <span>{value}</span>;
@ -140,11 +140,11 @@ ago
<div
className={class_name}
onClick={(e) => this.handleClick(e)}
onContextMenu={(e) => this.handleContextMenu(e)}
onContextMenu={(e) => this.handleContextMenu(e)}
>
{this.props.right_column && !this.props.nocontext
&& (
&& (
<div className="list__item__column list__item__column--right">
{
(this.props.right_column ? this.props.right_column.map((column, index) => (
@ -156,7 +156,7 @@ ago
{this.props.nocontext ? null : <ContextMenuTrigger className="list__item__column__item list__item__column__item--context-menu-trigger subtle" onTrigger={(e) => this.handleContextMenu(e)} />}
</div>
</div>
)}
<div className="list__item__column list__item__column--name">
@ -167,7 +167,7 @@ ago
{item.name !== undefined ? this.renderValue('name') : <span className="grey-text">{item.uri}</span>}
</div>
{this.props.details ? (
{this.props.details ? (
<ul className="list__item__column__item list__item__column__item--details details">
{
this.props.details.map((detail, index) => {
@ -184,11 +184,11 @@ ago
);
})
}
</ul>
</ul>
) : null}
</div>
{this.props.middle_column ? (
{this.props.middle_column ? (
<div className="list__item__column list__item__column--middle">
{
(this.props.middle_column ? this.props.middle_column.map((column, index) => (
@ -197,7 +197,7 @@ ago
</span>
)) : null)
}
</div>
</div>
) : null}
</div>
);

View File

@ -9,7 +9,7 @@ import VolumeControl from './Fields/VolumeControl';
import MuteControl from './Fields/MuteControl';
import OutputControl from './Fields/OutputControl';
import Dater from './Dater';
import ArtistSentence from './ArtistSentence';
import LinksSentence from './LinksSentence';
import Thumbnail from './Thumbnail';
import Icon from './Icon';
@ -18,7 +18,7 @@ import * as uiActions from '../services/ui/actions';
import * as coreActions from '../services/core/actions';
import * as mopidyActions from '../services/mopidy/actions';
class PlaybackControls extends React.Component {
class PlaybackControls extends React.Component {
constructor(props) {
super(props);
this.stream = null;
@ -31,7 +31,7 @@ class PlaybackControls extends React.Component {
}
componentDidMount() {
if (this.props.http_streaming_enabled) {
if (this.props.http_streaming_enabled) {
// Bust our cache, and by consequence, play our stream
this.props.coreActions.cachebustHttpStream();
}
@ -41,7 +41,7 @@ class PlaybackControls extends React.Component {
}
}
playStream(props = this.props) {
playStream(props = this.props) {
if (!this.stream) {
this.stream = new Audio();
} else {
@ -60,7 +60,7 @@ class PlaybackControls extends React.Component {
console.log(`Playing stream: ${this.stream.src}`);
}
componentWillReceiveProps(nextProps) {
componentWillReceiveProps(nextProps) {
// Cachebuster changed
// This happens when playback changes, so that the stream is "new", rather
// than the original stream. This prevents the browser cache from starting
@ -74,7 +74,7 @@ class PlaybackControls extends React.Component {
this.playStream(nextProps);
}
if (this.stream) {
if (this.stream) {
// Just been muted
if (this.props.http_streaming_mute !== nextProps.http_streaming_mute) {
this.stream.muted = nextProps.http_streaming_mute;
@ -104,7 +104,7 @@ class PlaybackControls extends React.Component {
// Images have just loaded
// A bit niggly to have to deeply check this...
if (this.props.current_track && nextProps.current_track) {
if (this.props.current_track && nextProps.current_track) {
if ((this.props.current_track.images && !nextProps.current_track.images) || (!this.props.current_track.images && nextProps.current_track.images)) {
this.setState({ current_track: nextProps.current_track });
}
@ -139,11 +139,11 @@ class PlaybackControls extends React.Component {
// Make sure there's enough distance between start and end before we handle
// this event as a 'tap'
if (this.start_position.x + tap_distance_threshold > end_position.x
&& this.start_position.x - tap_distance_threshold < end_position.x) {
&& this.start_position.x - tap_distance_threshold < end_position.x) {
// Scroll to top (without smooth_scroll)
helpers.scrollTo(null, false);
this.props.history.push('/queue');
} else {
} else {
// Swipe to the left = previous track
if (this.start_position.x < end_position.x) {
this.setTransition('previous');
@ -187,54 +187,54 @@ class PlaybackControls extends React.Component {
}
renderConsumeButton() {
let button = (
<button className="control tooltip" onClick={() => this.props.mopidyActions.setConsume(true)}>
<Icon name="restaurant" type="material" />
<span className="tooltip__content">Consume</span>
</button>
let button = (
<button className="control tooltip" onClick={() => this.props.mopidyActions.setConsume(true)}>
<Icon name="restaurant" type="material" />
<span className="tooltip__content">Consume</span>
</button>
);
if (this.props.consume) {
button = (
<button className="control control--active tooltip" onClick={() => this.props.mopidyActions.setConsume(false)}>
<Icon name="restaurant" type="material" />
<span className="tooltip__content">Consume</span>
</button>
button = (
<button className="control control--active tooltip" onClick={() => this.props.mopidyActions.setConsume(false)}>
<Icon name="restaurant" type="material" />
<span className="tooltip__content">Consume</span>
</button>
);
}
return button;
}
renderRandomButton() {
let button = (
<button className="control tooltip" onClick={() => this.props.mopidyActions.setRandom(true)}>
<Icon name="shuffle" type="material" />
<span className="tooltip__content">Shuffle</span>
</button>
let button = (
<button className="control tooltip" onClick={() => this.props.mopidyActions.setRandom(true)}>
<Icon name="shuffle" type="material" />
<span className="tooltip__content">Shuffle</span>
</button>
);
if (this.props.random) {
button = (
<button className="control control--active tooltip" onClick={() => this.props.mopidyActions.setRandom(false)}>
<Icon name="shuffle" type="material" />
<span className="tooltip__content">Shuffle</span>
</button>
button = (
<button className="control control--active tooltip" onClick={() => this.props.mopidyActions.setRandom(false)}>
<Icon name="shuffle" type="material" />
<span className="tooltip__content">Shuffle</span>
</button>
);
}
return button;
}
renderRepeatButton() {
let button = (
<button className="control tooltip" onClick={() => this.props.mopidyActions.setRepeat(true)}>
<Icon name="repeat" />
<span className="tooltip__content">Repeat</span>
</button>
let button = (
<button className="control tooltip" onClick={() => this.props.mopidyActions.setRepeat(true)}>
<Icon name="repeat" />
<span className="tooltip__content">Repeat</span>
</button>
);
if (this.props.repeat) {
button = (
<button className="control control--active tooltip" onClick={() => this.props.mopidyActions.setRepeat(false)}>
<Icon name="repeat" />
<span className="tooltip__content">Repeat</span>
</button>
button = (
<button className="control control--active tooltip" onClick={() => this.props.mopidyActions.setRepeat(false)}>
<Icon name="repeat" />
<span className="tooltip__content">Repeat</span>
</button>
);
}
return button;
@ -254,26 +254,26 @@ class PlaybackControls extends React.Component {
{next_track && next_track.images ? <Thumbnail className="hide" size="large" images={next_track.images} /> : null}
{this.state.transition_track && this.state.transition_direction ? (
{this.state.transition_track && this.state.transition_direction ? (
<div
className={`current-track current-track__transition current-track__transition--${this.state.transition_direction}`}
className={`current-track current-track__transition current-track__transition--${this.state.transition_direction}`}
>
<div className="text">
<div className="title">
{this.state.transition_track.name}
</div>
<div className="artist">
<ArtistSentence artists={this.state.transition_track.artists} nolinks />
<LinksSentence items={this.state.transition_track.artists} nolinks />
</div>
</div>
</div>
</div>
) : null}
<div
className={this.state.transition_track && this.state.transition_direction ? 'current-track current-track--transitioning' : 'current-track'}
onTouchStart={(e) => touch_enabled && this.handleTouchStart(e)}
onTouchEnd={(e) => touch_enabled && this.handleTouchEnd(e)}
tabIndex="-1"
tabIndex="-1"
>
<Link className="thumbnail-wrapper" to="/kiosk-mode" tabIndex="-1">
<Thumbnail size="small" images={images} />
@ -283,7 +283,7 @@ class PlaybackControls extends React.Component {
{current_track ? current_track.name : <span>-</span>}
</div>
<div className="artist">
{current_track ? <ArtistSentence artists={current_track.artists} /> : <ArtistSentence />}
{current_track ? <LinksSentence items={current_track.artists} /> : <LinksSentence />}
</div>
</div>
</div>
@ -314,13 +314,13 @@ class PlaybackControls extends React.Component {
<section className="volume">
<MuteControl
mute={this.props.mute}
onMuteChange={(mute) => this.props.mopidyActions.setMute(mute)}
onMuteChange={(mute) => this.props.mopidyActions.setMute(mute)}
/>
<VolumeControl
scrollWheel
volume={this.props.volume}
mute={this.props.mute}
onVolumeChange={(percent) => this.props.mopidyActions.setVolume(percent)}
onVolumeChange={(percent) => this.props.mopidyActions.setVolume(percent)}
/>
</section>
@ -366,4 +366,4 @@ const mapDispatchToProps = (dispatch) => ({
mopidyActions: bindActionCreators(mopidyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(PlaybackControls);
export default connect(mapStateToProps, mapDispatchToProps)(PlaybackControls);

View File

@ -3,7 +3,7 @@ import React from 'react';
import Link from './Link';
import Icon from './Icon';
import ArtistSentence from './ArtistSentence';
import LinksSentence from './LinksSentence';
import Dater from './Dater';
import URILink from './URILink';
import ContextMenuTrigger from './ContextMenuTrigger';
@ -183,7 +183,7 @@ export default class Track extends React.Component {
if (track.artists) {
track_details.push(
<li className="details__item details__item--artists" key="artists">
{track.artists ? <ArtistSentence artists={track.artists} /> : '-'}
{track.artists ? <LinksSentence items={track.artists} /> : '-'}
</li>,
);
}
@ -208,8 +208,7 @@ export default class Track extends React.Component {
{track.played_at ? (
<span>
<Dater type="ago" data={track.played_at} />
{' '}
ago
{' ago'}
</span>
) : '-'}
</div>
@ -220,15 +219,15 @@ ago
switch (type) {
case 'discover':
var link = <URILink type="recommendations" uri={helpers.getFromUri('seeds', track.added_from)}>discover</URILink>;
var link = <URILink type="recommendations" uri={helpers.getFromUri('seeds', track.added_from)}>Discover</URILink>;
break;
case 'browse':
var link = <URILink type="browse" uri={track.added_from.replace('iris:browse:', '')}>browse</URILink>;
var link = <URILink type="browse" uri={track.added_from.replace('iris:browse:', '')}>Browse</URILink>;
break;
case 'search':
var link = <URILink type="search" uri={track.added_from.replace('iris:', '')}>search</URILink>;
var link = <URILink type="search" uri={track.added_from.replace('iris:', '')}>Search</URILink>;
break;
case 'radio':
@ -236,18 +235,17 @@ ago
break;
default:
var link = <URILink type={type} uri={track.added_from}>{type}</URILink>;
var link = <URILink type={type} uri={track.added_from}>{helpers.titleCase(type)}</URILink>;
}
var track_middle_column = (
<div className="list__item__column__item list__item__column__item--added">
<span className="by">
{`${track.added_by} `}
</span>
<span className="from">
{`from `}
{link}
</span>
<span className="by by--with-spacing">
{`${track.added_by}`}
</span>
</div>
);
} else if (track.added_by) {
@ -299,7 +297,7 @@ ago
<div className="list__item__column list__item__column--name">
<div className="list__item__column__item--name">
{track.name ? track.name : <span className="mid_grey-text">{track.uri}</span>}
{track.explicit ? <span className="flag dark">EXPLICIT</span> : null}
{track.explicit ? <span className="flag flag--dark">EXPLICIT</span> : null}
{track.playing ? <Icon className={`js--${this.props.play_state}`} name="playing" type="css" /> : null}
</div>
{track_details ? (

View File

@ -1,15 +1,17 @@
import React, { memo } from 'react';
import Link from './Link';
import * as helpers from '../helpers';
export default memo((props) => {
export default memo((props) => {
let to = null;
let { uri } = props;
let { uri, type } = props;
const uriType = type || helpers.uriType(uri);
if (!props.unencoded) {
uri = encodeURIComponent(uri);
}
switch (props.type) {
switch (uriType) {
case 'playlist':
to = `/playlist/${uri}`;
break;
@ -52,7 +54,7 @@ export default memo((props) => {
<Link
className={props.className ? props.className : null}
to={to}
onContextMenu={(e) => (props.handleContextMenu ? props.handleContextMenu(e) : null)}
onContextMenu={(e) => (props.handleContextMenu ? props.handleContextMenu(e) : null)}
>
{props.children}
</Link>
@ -62,5 +64,5 @@ export default memo((props) => {
<span className={props.className ? props.className : null}>
{props.children}
</span>
);
});
);
});

View File

@ -7,7 +7,7 @@ import Header from '../components/Header';
import TrackList from '../components/TrackList';
import Thumbnail from '../components/Thumbnail';
import Parallax from '../components/Parallax';
import ArtistSentence from '../components/ArtistSentence';
import LinksSentence from '../components/LinksSentence';
import Loader from '../components/Loader';
import FollowButton from '../components/Fields/FollowButton';
import NiceNumber from '../components/NiceNumber';
@ -186,7 +186,7 @@ export class Album extends React.Component {
) : null}
{album.artists && album.artists.length > 0 ? (
<li>
<ArtistSentence artists={album.artists} />
<LinksSentence items={album.artists} />
</li>
) : null}
{album.release_date ? (

View File

@ -7,7 +7,7 @@ import Icon from '../components/Icon';
import Parallax from '../components/Parallax';
import TrackList from '../components/TrackList';
import Dater from '../components/Dater';
import ArtistSentence from '../components/ArtistSentence';
import LinksSentence from '../components/LinksSentence';
import Thumbnail from '../components/Thumbnail';
import Header from '../components/Header';
import URILink from '../components/URILink';
@ -112,24 +112,6 @@ class Queue extends React.Component {
this.props.mopidyActions.reorderTracklist(indexes, index);
}
renderQueueStats() {
const { current_tracklist } = this.props;
return (
<div className="queue-stats mid_grey-text">
<span>
{`${current_tracklist.length || 0} tracks`}
</span>
&nbsp;&nbsp;|&nbsp;&nbsp;
{current_tracklist.length > 0 ? (
<Dater type="total-time" data={current_tracklist} />
) : (
<span>0 mins</span>
)}
</div>
);
}
renderArtwork(image) {
const { current_track } = this.props;
@ -147,7 +129,7 @@ class Queue extends React.Component {
}
return (
<div className="current-track__artwork">
<URILink type="album" uri={uri}>
<URILink uri={uri}>
<Thumbnail glow image={image} />
</URILink>
</div>
@ -159,44 +141,44 @@ class Queue extends React.Component {
if (!added_from_uri) return null;
const uri_type = helpers.uriType(added_from_uri);
let item_uri = added_from_uri;
let item_type = uri_type;
const items = [];
// Radio nests it's seed URIs in an encoded URI format
if (uri_type === 'radio') {
const radio_seeds = helpers.getFromUri('seeds', added_from_uri);
// For now we only care about the first seed
// TODO: Support for multiple seeds
item_uri = radio_seeds[0];
item_type = helpers.uriType(item_uri);
for (let seed of radio_seeds) {
let item_type = helpers.uriType(seed);
let item_library = this.props[`${item_type}s`];
if (item_library && item_library[seed]) {
items.push(item_library[seed]);
}
}
} else {
const item_library = this.props[`${uri_type}s`];
if (item_library && item_library[added_from_uri]) {
items.push(item_library[added_from_uri]);
}
}
const item_library = this.props[`${item_type}s`];
if (!item_library) return null;
const item = item_library[item_uri];
if (!item) return null;
if (items.length <= 0) return null;
return (
<div className="current-track__added-from">
<URILink
type={item_type}
uri={item.uri}
uri={items[0].uri}
className="current-track__added-from__thumbnail"
>
<Thumbnail
images={item.images}
images={items[0].images}
size="small"
circle={item_type === 'artist'}
circle={helpers.uriType(items[0].uri) === 'artist'}
/>
</URILink>
<div className="current-track__added-from__text">
{'Playing from '}
<URILink type={item_type} uri={item.uri}>
{item.name}
</URILink>
{uri_type === 'radio' && <span> radio</span>}
<LinksSentence items={items} />
{uri_type === 'radio' && <span className="flag flag--blue">Radio</span>}
</div>
</div>
);
@ -217,15 +199,12 @@ class Queue extends React.Component {
const options = (
<span>
{this.props.spotify_enabled ? (
{this.props.spotify_enabled && (
<Link className="button button--no-hover" to="/queue/radio">
<Icon name="radio" />
Radio
{this.props.radio && this.props.radio.enabled ? (
<span className="flag blue">On</span>
) : null}
</Link>
) : null}
)}
<Link className="button button--no-hover" to="/queue/history">
<Icon name="history" />
History
@ -259,12 +238,12 @@ class Queue extends React.Component {
</div>
{current_track ? (
<ArtistSentence
<LinksSentence
className="current-track__artists"
artists={current_track.artists}
items={current_track.artists}
/>
) : (
<ArtistSentence className="current-track__artists" />
<LinksSentence className="current-track__artists" />
)}
{this.renderAddedFrom()}
@ -273,17 +252,14 @@ class Queue extends React.Component {
<ul className="details">
<li>{`${queue_tracks.length} tracks`}</li>
<li><Dater type="total-time" data={queue_tracks} /></li>
<li>
<a
onClick={(e) => {
this.props.mopidyActions.clearTracklist();
this.props.uiActions.hideContextMenu();
}}
>
<Icon name="delete_sweep" />
Clear queue
</a>
</li>
{queue_tracks.length > 0 && (
<li>
<a onClick={e => this.props.mopidyActions.clearTracklist()}>
<Icon name="delete_sweep" />
Clear queue
</a>
</li>
)}
</ul>
</div>
</div>

View File

@ -448,12 +448,12 @@ Advanced
{' '}
installed
{this.props.pusher.version.upgrade_available ? (
<span className="flag dark">
<span className="flag flag--dark">
<Icon name="cloud_download" className="blue-text" />
&nbsp; Upgrade available
</span>
) : (
<span className="flag dark">
<span className="flag flag--dark">
<Icon name="check" className="green-text" />
&nbsp; Up-to-date
</span>

View File

@ -8,7 +8,7 @@ import ErrorMessage from '../components/ErrorMessage';
import Header from '../components/Header';
import TrackList from '../components/TrackList';
import Thumbnail from '../components/Thumbnail';
import ArtistSentence from '../components/ArtistSentence';
import LinksSentence from '../components/LinksSentence';
import ArtistGrid from '../components/ArtistGrid';
import FollowButton from '../components/Fields/FollowButton';
import LastfmLoveButton from '../components/Fields/LastfmLoveButton';
@ -232,13 +232,13 @@ Could not find track with URI "
{track.album && !track.album.uri ? track.album.name : null}
{!track.album ? 'Unknown album' : null}
{' by '}
<ArtistSentence artists={track.artists} />
<LinksSentence items={track.artists} />
</h2>
<ul className="details">
{!this.props.slim_mode ? <li className="source"><Icon type="fontawesome" name={helpers.sourceIcon(this.props.uri)} /></li> : null}
{track.date ? <li><Dater type="date" data={track.date} /></li> : null}
{track.explicit ? <li><span className="flag dark">EXPLICIT</span></li> : null}
{track.explicit ? <li><span className="flag flag--dark">EXPLICIT</span></li> : null}
<li>
{track.disc_number > 0 && <span>Disc {track.disc_number}</span>}
{track.disc_number > 0 && track.track_number > 0 && <span>,&nbsp;</span>}

View File

@ -5,7 +5,7 @@ import { bindActionCreators } from 'redux';
import InputRange from 'react-input-range';
import Header from '../../components/Header';
import ArtistSentence from '../../components/ArtistSentence';
import LinksSentence from '../../components/LinksSentence';
import ArtistGrid from '../../components/ArtistGrid';
import AlbumGrid from '../../components/AlbumGrid';
import TrackList from '../../components/TrackList';
@ -23,7 +23,7 @@ import * as uiActions from '../../services/ui/actions';
import * as mopidyActions from '../../services/mopidy/actions';
import * as spotifyActions from '../../services/spotify/actions';
class Discover extends React.Component {
class Discover extends React.Component {
constructor(props) {
super(props);
@ -157,14 +157,14 @@ class Discover extends React.Component {
}
}
componentWillReceiveProps(newProps, newState) {
componentWillReceiveProps(newProps, newState) {
// New seeds via URL
if (newProps.match.params.seeds != this.props.match.params.seeds) {
this.handleURLSeeds(newProps.match.params.seeds);
}
}
handleContextMenu(e) {
handleContextMenu(e) {
var uri = 'iris:discover';
if (this.state.seeds) {
uri += ':';
@ -195,14 +195,14 @@ class Discover extends React.Component {
this.props.uiActions.showContextMenu(data);
}
handleURLSeeds(seeds_string = this.props.match.params.seeds) {
handleURLSeeds(seeds_string = this.props.match.params.seeds) {
// 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(',');
for (let i = 0; i < seeds.length; i++) {
switch (helpers.uriType(seeds[i])) {
switch (helpers.uriType(seeds[i])) {
case 'artist':
this.props.spotifyActions.getArtist(seeds[i]);
break;
@ -263,7 +263,7 @@ class Discover extends React.Component {
for (let i = 0; i < this.state.seeds.length; i++) {
const uri = this.state.seeds[i];
switch (helpers.uriType(uri)) {
switch (helpers.uriType(uri)) {
case 'track':
if (typeof (this.props.tracks[uri]) !== 'undefined') {
seeds_objects.push(this.props.tracks[uri]);
@ -345,7 +345,7 @@ class Discover extends React.Component {
const addable_tunabilities = [];
const enabled_tunabilities = [];
for (const key in this.state.tunabilities) {
if (this.state.tunabilities.hasOwnProperty(key)) {
if (this.state.tunabilities.hasOwnProperty(key)) {
const tunability = {
...this.state.tunabilities[key],
@ -380,7 +380,7 @@ class Discover extends React.Component {
minValue={tunability.min}
maxValue={tunability.max}
value={tunability.value}
onChange={(value) => this.setTunability(tunability.name, value)}
onChange={(value) => this.setTunability(tunability.name, value)}
/>
</div>
</div>
@ -390,7 +390,7 @@ class Discover extends React.Component {
);
}
renderResults() {
renderResults() {
// Results not in
if (!this.props.recommendations || this.props.recommendations.albums_uris === undefined || this.props.recommendations.artists_uris === undefined) {
return <div className="content-wrapper recommendations-results" />;
@ -479,7 +479,7 @@ class Discover extends React.Component {
const is_loading = helpers.isLoading(this.props.load_queue, ['spotify_recommendations']);
const addable_tunabilities = [];
for (const key in this.state.tunabilities) {
if (this.state.tunabilities.hasOwnProperty(key)) {
if (this.state.tunabilities.hasOwnProperty(key)) {
const tunability = {
...this.state.tunabilities[key],
@ -516,7 +516,7 @@ class Discover extends React.Component {
<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); }} />
<span className={`submit button button--primary button--large${is_loading ? ' button--working' : ''}`} onClick={(e) => this.getRecommendations()}>
<Icon name="explore" />
<Icon name="explore" />
&nbsp;
Find recommendations
</span>
@ -551,4 +551,4 @@ const mapDispatchToProps = (dispatch) => ({
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Discover);
export default connect(mapStateToProps, mapDispatchToProps)(Discover);

View File

@ -79,7 +79,7 @@ class EditRadio extends React.Component {
if (valid_seeds) {
this.props.pusherActions.updateRadio(this.state.seeds);
this.props.uiActions.closeModal();
window.history.back();
} else {
this.setState({ error_message: 'Invalid seed URI(s)' });
}

View File

@ -6,7 +6,7 @@ import Link from '../../components/Link';
import Modal from './Modal';
import Thumbnail from '../../components/Thumbnail';
import ArtistSentence from '../../components/ArtistSentence';
import LinksSentence from '../../components/LinksSentence';
import Dater from '../../components/Dater';
import Icon from '../../components/Icon';
import ProgressSlider from '../../components/Fields/ProgressSlider';
@ -15,7 +15,7 @@ import * as helpers from '../../helpers';
import * as uiActions from '../../services/ui/actions';
import * as mopidyActions from '../../services/mopidy/actions';
class KioskMode extends React.Component {
class KioskMode extends React.Component {
constructor(props) {
super(props);
}
@ -73,7 +73,7 @@ class KioskMode extends React.Component {
<div className="player">
<div className="current-track">
<div className="title">{ this.props.current_track ? this.props.current_track.name : <span>-</span> }</div>
{ this.props.current_track ? <ArtistSentence nolinks artists={this.props.current_track.artists} /> : <ArtistSentence /> }
{ this.props.current_track ? <LinksSentence nolinks items={this.props.current_track.artists} /> : <LinksSentence /> }
</div>
<div className="progress-wrapper">
@ -95,4 +95,4 @@ const mapDispatchToProps = (dispatch) => ({
mopidyActions: bindActionCreators(mopidyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(KioskMode);
export default connect(mapStateToProps, mapDispatchToProps)(KioskMode);