Track now 100% functional component

This commit is contained in:
James Barnsley
2021-08-15 11:55:20 +12:00
parent 3c055724b6
commit 4a3e4e75fc
7 changed files with 1706 additions and 1972 deletions

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import Icon, { SourceIcon } from './Icon';
import LinksSentence from './LinksSentence';
import { Dater, dater } from './Dater';
@ -13,34 +13,139 @@ import {
} from '../util/helpers';
import { I18n } from '../locale';
export default class Track extends React.Component {
constructor(props) {
super(props);
const MiddleColumn = ({
track_context,
item: {
added_from,
added_by,
played_at,
} = {},
}) => {
let content;
this.state = {
hover: false,
};
switch (track_context) {
case 'history': {
content = (
<div className="list__item__column__item list__item__column__item--played_at">
{
played_at ? (
<I18n path="specs.played_ago" time={dater('ago', played_at)} />
) : ('-')
}
</div>
);
break;
}
this.key = props.buildTrackKey(props.item, props.getItemIndex());
this.start_time = 0;
this.end_time = 0;
this.start_position = false;
case 'queue': {
if (added_from) {
const type = (added_from ? uriType(added_from) : null);
let link = null;
switch (type) {
case 'discover':
link = (
<URILink type="recommendations" uri={getFromUri('seeds', added_from)}>
<I18n path="discover.title" />
</URILink>
);
break;
case 'browse':
link = (
<URILink type={type} uri={added_from}>
<I18n path="library.browse.title" />
</URILink>
);
break;
case 'search':
link = (
<URILink type={type} uri={added_from}>
<I18n path="search.title" />
</URILink>
);
break;
case 'radio':
link = <I18n path="modal.edit_radio.title" />;
break;
case 'queue-history':
link = <I18n path="queue_history.title" />;
break;
default:
link = <URILink type={type} uri={added_from}>{titleCase(type)}</URILink>;
}
content = (
<div className="list__item__column__item list__item__column__item--added">
<span className="from">
{link}
</span>
{added_by && (
<span className="by by--with-spacing">
{`${added_by}`}
</span>
)}
</div>
);
} else if (added_by) {
content = (
<div className="list__item__column__item list__item__column__item--added">
<span className="by">{added_by}</span>
</div>
);
}
break;
}
default:
return null;
}
handleMouseEnter = () => {
this.setState({ hover: true });
}
return (
<div className="list__item__column list__item__column--middle">
{content}
</div>
);
}
handleMouseLeave = () => {
this.setState({ hover: false });
}
const Track = ({
item,
track_context,
stream_title,
play_state,
selected_tracks,
can_sort,
show_source_icon,
getItemIndex,
handleContextMenu,
handleDrag,
handleDrop,
handleClick,
handleDoubleClick,
handleDoubleTap,
handleTouchDrag,
handleTap,
dragger,
buildTrackKey,
}) => {
const [hover, setHover] = useState(false);
const key = buildTrackKey(item, getItemIndex());
const [currentEvent, setCurrentEvent] = useState({
start_time: 0,
end_time: 0,
});
handleContextMenu = (e) => {
const { handleContextMenu } = this.props;
handleContextMenu(e, this.key);
}
const onMouseEnter = () => setHover(true);
const onMouseLeave = () => setHover(false);
const onContextMenu = (e) => handleContextMenu(e, key);
const onDoubleClick = (e) => handleDoubleClick(e, key);
const updateCurrentEvent = (data) => setCurrentEvent({ ...currentEvent, ...data });
handleMouseDown = (e) => {
const onMouseDown = (e) => {
const target = $(e.target);
// Clicked a nested link (ie Artist name), so no dragging required
@ -50,27 +155,25 @@ export default class Track extends React.Component {
// Only listen for left mouse clicks
if (e.button === 0) {
this.start_position = {
x: e.pageX,
y: e.pageY,
};
updateCurrentEvent({
start_position: {
x: e.pageX,
y: e.pageY,
},
});
// Not left click, then ensure no dragging
} else {
this.start_position = false;
updateCurrentEvent({ start_position: false });
}
}
handleMouseMove = (e) => {
const {
handleDrag,
} = this.props;
const onMouseMove = (e) => {
if (handleDrag === undefined) return false;
if (this.start_position) {
const start_x = this.start_position.x;
const start_y = this.start_position.y;
if (currentEvent.start_position) {
const start_x = currentEvent.start_position.x;
const start_y = currentEvent.start_position.y;
const threshold = 5;
// Have we dragged outside of our threshold zone?
@ -81,18 +184,13 @@ export default class Track extends React.Component {
|| e.pageY < start_y - threshold
) {
// Handover to parent for dragging. We can unset all our behaviour now.
handleDrag(e, this.key);
this.start_position = false;
handleDrag(e, key);
updateCurrentEvent({ start_position: false });
}
}
}
};
handleMouseUp = (e) => {
const {
handleDrop,
handleClick,
dragger,
} = this.props;
const onMouseUp = (e) => {
const target = $(e.target);
// Only listen for left clicks
@ -101,73 +199,62 @@ export default class Track extends React.Component {
e.preventDefault();
if (handleDrop !== undefined) {
handleDrop(e, this.key);
handleDrop(e, key);
}
} else if (!target.is('a') && target.closest('a').length <= 0) {
handleClick(e, this.key);
this.start_position = false;
handleClick(e, key);
updateCurrentEvent({ start_position: false });
}
return;
}
// Not left click, then ensure no dragging
this.start_position = false;
updateCurrentEvent({ start_position: false });
}
handleDoubleClick = (e) => {
const { handleDoubleClick } = this.props;
handleDoubleClick(e, this.key);
}
handleTouchStart = (e) => {
const { handleTouchDrag } = this.props;
const onTouchStart = (e) => {
const target = $(e.target);
const timestamp = Math.floor(Date.now());
if (target.hasClass('drag-zone')) {
handleTouchDrag(e, this.key);
handleTouchDrag(e, key);
e.preventDefault();
}
// Save touch start details
this.start_time = timestamp;
this.start_position = {
x: e.touches[0].clientX,
y: e.touches[0].clientY,
};
updateCurrentEvent({
start_time: timestamp,
start_position: {
x: e.touches[0].clientX,
y: e.touches[0].clientY,
},
});
return false;
}
handleTouchEnd = (e) => {
const {
handleDoubleTap,
handleContextMenu,
handleTap,
} = this.props;
const onTouchEnd = (e) => {
const target = $(e.target);
const timestamp = Math.floor(Date.now());
const tap_distance_threshold = 10; // Max distance (px) between touchstart and touchend to qualify as a tap
const tap_time_threshold = 200; // Max time (ms) between touchstart and touchend to qualify as a tap
const tap_distance_threshold = 10; // Max distance (px) between touchstart and touchend to qualify as a tap
const tap_time_threshold = 200; // Max time (ms) between touchstart and touchend to qualify as a tap
const end_position = {
x: e.changedTouches[0].clientX,
y: e.changedTouches[0].clientY,
};
// Too long between touchstart and touchend
if (this.start_time + tap_time_threshold < timestamp) {
if (currentEvent.start_time + tap_time_threshold < timestamp) {
return false;
}
// 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.y + tap_distance_threshold > end_position.y
&& this.start_position.y - tap_distance_threshold < end_position.y
currentEvent.start_position.x + tap_distance_threshold > end_position.x
&& currentEvent.start_position.x - tap_distance_threshold < end_position.x
&& currentEvent.start_position.y + tap_distance_threshold > end_position.y
&& currentEvent.start_position.y - tap_distance_threshold < end_position.y
) {
// Clicked a nested link (ie Artist name), so no dragging required
if (!target.is('a')) {
@ -178,246 +265,132 @@ export default class Track extends React.Component {
if (target.hasClass('touch-contextable')) {
// Update our selection. By not passing touch = true selection will work like a regular
// click this.props.handleSelection(e);
handleContextMenu(e, this.key);
handleContextMenu(e, key);
return false;
}
// We received a touchend within 300ms ago, so handle as double-tap
if ((timestamp - this.end_time) > 0 && (timestamp - this.end_time) <= 300) {
handleDoubleTap(e, this.key);
if ((timestamp - currentEvent.end_time) > 0 && (timestamp - currentEvent.end_time) <= 300) {
handleDoubleTap(e, key);
e.preventDefault();
return false;
}
handleTap(e, this.key);
handleTap(e, key);
}
this.end_time = timestamp;
updateCurrentEvent({ end_time: timestamp });
}
renderTrackMiddleColumn = () => {
const {
track_context,
item: {
added_from,
added_by,
played_at,
} = {},
} = this.props;
if (!item) return null;
let content;
let className = 'list__item list__item--track mouse-draggable mouse-selectable mouse-contextable';
const track_details = [];
switch (track_context) {
case 'history': {
content = (
<div className="list__item__column__item list__item__column__item--played_at">
{
played_at ? (
<I18n path="specs.played_ago" time={dater('ago', played_at)} />
) : ('-')
}
</div>
);
break;
}
if (item.artists) {
track_details.push(
<li className="details__item details__item--artists" key="artists">
{item.artists ? <LinksSentence items={item.artists} type="artist" /> : '-'}
</li>,
);
} else if (item.playing && stream_title) {
track_details.push(
<li className="details__item details__item--artists" key="stream_title">
<span className="links-sentence">{stream_title}</span>
</li>,
);
}
case 'queue': {
if (added_from) {
const type = (added_from ? uriType(added_from) : null);
switch (type) {
case 'discover':
var link = (
<URILink type="recommendations" uri={getFromUri('seeds', added_from)}>
<I18n path="discover.title" />
</URILink>
);
break;
case 'browse':
var link = (
<URILink type={type} uri={added_from}>
<I18n path="library.browse.title" />
</URILink>
);
break;
case 'search':
var link = (
<URILink type={type} uri={added_from}>
<I18n path="search.title" />
</URILink>
);
break;
case 'radio':
var link = <I18n path="modal.edit_radio.title" />;
break;
case 'queue-history':
var link = <I18n path="queue_history.title" />;
break;
default:
var link = <URILink type={type} uri={added_from}>{titleCase(type)}</URILink>;
}
content = (
<div className="list__item__column__item list__item__column__item--added">
<span className="from">
{link}
</span>
{added_by && (
<span className="by by--with-spacing">
{`${added_by}`}
</span>
)}
</div>
);
} else if (added_by) {
content = (
<div className="list__item__column__item list__item__column__item--added">
<span className="by">{added_by}</span>
</div>
);
if (item.album) {
track_details.push(
<li className="details__item details__item--album" key="album">
{item.album.uri
? <URILink type="album" uri={item.album.uri}>{item.album.name}</URILink>
: <span>{item.album.name}</span>
}
break;
}
default:
return null;
}
return (
<div className="list__item__column list__item__column--middle">
{content}
</div>
</li>,
);
}
render = () => {
const {
item,
track_context,
stream_title,
play_state,
selected_tracks,
can_sort,
show_source_icon,
} = this.props;
const {
hover,
} = this.state;
// If we're touchable, and can sort this tracklist
let drag_zone = null;
if (isTouchDevice() && can_sort) {
className += ' list__item--has-drag-zone';
if (!item) return null;
drag_zone = (
<span
className="list__item__column__item list__item__column__item--drag-zone drag-zone touch-draggable mouse-draggable"
key="drag-zone"
>
<Icon name="drag_indicator" />
</span>
);
}
let className = 'list__item list__item--track mouse-draggable mouse-selectable mouse-contextable';
const track_details = [];
const track_middle_column = <MiddleColumn track_context={track_context} item={item} />;
if (selected_tracks.includes(key)) className += ' list__item--selected';
if (can_sort) className += ' list__item--can-sort';
if (item.type !== undefined) className += ` list__item--${item.type}`;
if (item.playing) className += ' list__item--playing';
if (item.loading) className += ' list__item--loading';
if (hover) className += ' list__item--hover';
if (track_middle_column) className += ' list__item--has-middle-column';
if (track_details.length > 0) className += ' list__item--has-details';
if (item.artists) {
track_details.push(
<li className="details__item details__item--artists" key="artists">
{item.artists ? <LinksSentence items={item.artists} type="artist" /> : '-'}
</li>,
);
} else if (item.playing && stream_title) {
track_details.push(
<li className="details__item details__item--artists" key="stream_title">
<span className="links-sentence">{stream_title}</span>
</li>,
);
}
if (item.album) {
if (item.album.uri) {
var album = <URILink type="album" uri={item.album.uri}>{item.album.name}</URILink>;
} else {
var album = <span>{item.album.name}</span>;
}
track_details.push(
<li className="details__item details__item--album" key="album">
{album}
</li>,
);
}
// If we're touchable, and can sort this tracklist
let drag_zone = null;
if (isTouchDevice() && can_sort) {
className += ' list__item--has-drag-zone';
drag_zone = (
<span
className="list__item__column__item list__item__column__item--drag-zone drag-zone touch-draggable mouse-draggable"
key="drag-zone"
>
<Icon name="drag_indicator" />
</span>
);
}
const track_middle_column = this.renderTrackMiddleColumn();
if (selected_tracks.includes(this.key)) className += ' list__item--selected';
if (can_sort) className += ' list__item--can-sort';
if (item.type !== undefined) className += ` list__item--${item.type}`;
if (item.playing) className += ' list__item--playing';
if (item.loading) className += ' list__item--loading';
if (hover) className += ' list__item--hover';
if (track_middle_column) className += ' list__item--has-middle-column';
if (track_details.length > 0) className += ' list__item--has-details';
return (
<ErrorBoundary>
<div
className={className}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
onMouseMove={this.handleMouseMove}
onDoubleClick={this.handleDoubleClick}
onContextMenu={this.handleContextMenu}
onTouchStart={this.handleTouchStart}
onTouchEnd={this.handleTouchEnd}
>
<div className="list__item__column list__item__column--name">
<div className="list__item__column__item--name">
{item.name ? item.name : <span className="mid_grey-text">{item.uri}</span>}
{item.playing && <Icon className={`js--${play_state}`} name="playing" type="css" />}
</div>
{track_details && (
<ul className="list__item__column__item--details">
{track_details}
</ul>
)}
</div>
{track_middle_column}
<div className="list__item__column list__item__column--right">
{drag_zone}
{item.is_explicit && <span className="flag flag--dark">EXPLICIT</span>}
{(track_context === 'album' || track_context === 'artist') && item.track_number && (
<span className="mid_grey-text list__item__column__item list__item__column__item--track-number">
<span>
<I18n path="track.title" />
&nbsp;
</span>
{item.track_number}
</span>
)}
<span className="list__item__column__item list__item__column__item--duration">
{item.duration ? <Dater type="length" data={item.duration} /> : '-'}
</span>
{show_source_icon && (
<span className="list__item__column__item list__item__column__item--source">
<SourceIcon uri={item.uri} fixedWidth />
</span>
)}
<ContextMenuTrigger className="list__item__column__item--context-menu-trigger subtle" onTrigger={(e) => this.props.handleContextMenu(e)} />
return (
<ErrorBoundary>
<div
className={className}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onMouseMove={onMouseMove}
onDoubleClick={onDoubleClick}
onContextMenu={onContextMenu}
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
>
<div className="list__item__column list__item__column--name">
<div className="list__item__column__item--name">
{item.name ? item.name : <span className="mid_grey-text">{item.uri}</span>}
{item.playing && <Icon className={`js--${play_state}`} name="playing" type="css" />}
</div>
{track_details && (
<ul className="list__item__column__item--details">
{track_details}
</ul>
)}
</div>
</ErrorBoundary>
);
}
{track_middle_column}
<div className="list__item__column list__item__column--right">
{drag_zone}
{item.is_explicit && <span className="flag flag--dark">EXPLICIT</span>}
{(track_context === 'album' || track_context === 'artist') && item.track_number && (
<span className="mid_grey-text list__item__column__item list__item__column__item--track-number">
<span>
<I18n path="track.title" />
&nbsp;
</span>
{item.track_number}
</span>
)}
<span className="list__item__column__item list__item__column__item--duration">
{item.duration ? <Dater type="length" data={item.duration} /> : '-'}
</span>
{show_source_icon && (
<span className="list__item__column__item list__item__column__item--source">
<SourceIcon uri={item.uri} fixedWidth />
</span>
)}
<ContextMenuTrigger
className="list__item__column__item--context-menu-trigger subtle"
onTrigger={handleContextMenu}
/>
</div>
</div>
</ErrorBoundary>
);
}
export default Track;

View File

@ -54,7 +54,7 @@ const Album = ({
const [filter, setFilter] = useState('');
const [album, setAlbum] = useState({});
let tracks = album ?.tracks || [];
let tracks = album?.tracks || [];
if (sortField && tracks) tracks = sortItems(tracks, sortField, sortReverse);
if (filter && filter !== '') tracks = applyFilter('name', filter, tracks);
@ -101,16 +101,12 @@ const Album = ({
);
}
const handleContextMenu = (e) => {
showContextMenu({
e,
context: 'album',
items: [album],
uris: [uri],
});
}
const play = () => playURIs([uri], uri);
const handleContextMenu = (e) => showContextMenu({
e,
context: 'album',
items: [album],
uris: [uri],
});
const onChangeSort = (field) => {
let reverse = false;
@ -120,7 +116,7 @@ const Album = ({
setSort(SORT_KEY, field, reverse);
hideContextMenu();
}
};
const sort_options = [
{
@ -193,7 +189,7 @@ const Album = ({
<div className="actions">
<Button
type="primary"
onClick={play}
onClick={() => playURIs([uri], uri)}
tracking={{ category: 'Album', action: 'Play' }}
>
<I18n path="actions.play" />

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Link from '../components/Link';
@ -10,7 +10,6 @@ import LinksSentence from '../components/LinksSentence';
import Thumbnail from '../components/Thumbnail';
import Header from '../components/Header';
import URILink from '../components/URILink';
import LazyLoadListener from '../components/LazyLoadListener';
import * as coreActions from '../services/core/actions';
import * as uiActions from '../services/ui/actions';
import * as pusherActions from '../services/pusher/actions';
@ -50,47 +49,92 @@ const Artwork = ({
);
};
class Queue extends React.Component {
componentDidMount() {
const {
uiActions: {
setWindowTitle,
},
location: {
limit,
} = {},
} = this.props;
const AddedFrom = ({
items,
added_from_uri,
}) => {
if (!added_from_uri) return null;
if (limit) this.setState({ limit });
setWindowTitle(i18n('now_playing.title'));
const uri_type = uriType(added_from_uri);
let addedFromItems = [];
// Radio nests it's seed URIs in an encoded URI format
switch (uri_type) {
case 'radio':
addedFromItems = indexToArray(items, getFromUri('seeds', added_from_uri));
break;
case 'search':
addedFromItems = [{
uri: added_from_uri,
name: `"${getFromUri('searchterm', added_from_uri)}" search`,
}];
break;
default:
addedFromItems = indexToArray(items, [added_from_uri]);
break;
}
shouldComponentUpdate(nextProps) {
return nextProps !== this.props;
}
if (!addedFromItems.length) return null;
componentDidUpdate = ({
added_from_uri: prev_added_from_uri,
}) => {
const {
coreActions: {
loadUri,
},
added_from_uri,
} = this.props;
return (
<div className="current-track__added-from">
{addedFromItems[0].images && addedFromItems[0].uri && (
<URILink
uri={addedFromItems[0].uri}
type={addedFromItems[0].type}
className="current-track__added-from__thumbnail"
>
<Thumbnail
images={addedFromItems[0].images}
size="small"
circle={uriType(addedFromItems[0].uri) === 'artist'}
type="artist"
/>
</URILink>
)}
<div className="current-track__added-from__text">
{'Playing from '}
<LinksSentence
items={addedFromItems}
type={addedFromItems[0].type}
/>
{uri_type === 'radio' && (
<span className="flag flag--blue">
{i18n('now_playing.current_track.radio')}
</span>
)}
</div>
</div>
);
};
if (added_from_uri && added_from_uri !== prev_added_from_uri) {
loadUri(added_from_uri);
}
}
const Queue = ({
queue_tracks,
items,
added_from_uri,
current_track,
stream_title,
theme,
current_track_uri,
spotify_enabled,
uiActions: uiActionsProp, // TODO: Remove <Header>'s dependency on passing this
coreActions: {
loadUri,
},
mopidyActions: {
removeTracks,
changeTrack,
reorderTracklist,
clearTracklist,
shuffleTracklist,
},
}) => {
useEffect(() => uiActionsProp.setWindowTitle(i18n('now_playing.title')), []);
useEffect(() => {
if (added_from_uri) loadUri(added_from_uri);
}, [added_from_uri])
removeTracks = (track_indexes) => {
const {
queue_tracks,
mopidyActions: {
removeTracks: doRemoveTracks,
},
} = this.props;
const onRemoveTracks = (track_indexes) => {
const tlids = [];
for (let i = 0; i < track_indexes.length; i++) {
const track = queue_tracks[track_indexes[i]];
@ -100,201 +144,117 @@ class Queue extends React.Component {
}
if (tlids.length > 0) {
doRemoveTracks(tlids);
removeTracks(tlids);
}
}
playTrack = (track) => {
const { mopidyActions: { changeTrack } } = this.props;
changeTrack(track.tlid);
}
const onPlayTrack = (track) => changeTrack(track.tlid);
const onPlayTracks = (tracks) => changeTrack(tracks[0].tlid);
const onReorderTracks = (indexes, index) => reorderTracklist(indexes, index);
playTracks = (tracks) => {
const { mopidyActions: { changeTrack } } = this.props;
changeTrack(tracks[0].tlid);
}
reorderTracks = (indexes, index) => {
const { mopidyActions: { reorderTracklist } } = this.props;
reorderTracklist(indexes, index);
}
renderAddedFrom = () => {
const {
items,
added_from_uri,
} = this.props;
if (!added_from_uri) return null;
const uri_type = uriType(added_from_uri);
let addedFromItems = [];
// Radio nests it's seed URIs in an encoded URI format
switch (uri_type) {
case 'radio':
addedFromItems = indexToArray(items, getFromUri('seeds', added_from_uri));
break;
case 'search':
addedFromItems = [{
uri: added_from_uri,
name: `"${getFromUri('searchterm', added_from_uri)}" search`,
}];
break;
default:
addedFromItems = indexToArray(items, [added_from_uri]);
break;
let current_track_image = null;
if (current_track && current_track_uri) {
if (current_track.images !== undefined && current_track.images) {
current_track_image = current_track.images.large;
}
}
if (!addedFromItems.length) return null;
const options = (
<>
{spotify_enabled && (
<Button noHover discrete to="/queue/radio">
<Icon name="radio" />
<I18n path="now_playing.context_actions.radio" />
</Button>
)}
<Button noHover discrete to="/queue/history">
<Icon name="history" />
<I18n path="now_playing.context_actions.history" />
</Button>
<Button noHover discrete to="/queue/add-uri">
<Icon name="playlist_add" />
<I18n path="actions.add" />
</Button>
</>
);
return (
<div className="current-track__added-from">
{addedFromItems[0].images && addedFromItems[0].uri && (
<URILink
uri={addedFromItems[0].uri}
type={addedFromItems[0].type}
className="current-track__added-from__thumbnail"
>
<Thumbnail
images={addedFromItems[0].images}
size="small"
circle={uriType(addedFromItems[0].uri) === 'artist'}
type="artist"
/>
</URILink>
)}
<div className="current-track__added-from__text">
{'Playing from '}
<LinksSentence
items={addedFromItems}
type={addedFromItems[0].type}
return (
<div className="view queue-view preserve-3d">
<Header options={options} uiActions={uiActionsProp}>
<Icon name="play_arrow" type="material" />
<I18n path="now_playing.title" />
</Header>
{theme === 'dark' && <Parallax image={current_track_image} blur />}
<div className="content-wrapper">
<div className="current-track">
<Artwork
image={current_track_image}
album_uri={current_track && current_track.album && current_track.album.uri}
/>
{uri_type === 'radio' && (
<span className="flag flag--blue">
{i18n('now_playing.current_track.radio')}
</span>
)}
</div>
</div>
);
}
<div className="current-track__details">
<div className="current-track__title">
{stream_title && (
<span>{stream_title}</span>
)}
{!stream_title && current_track && (
<URILink type="track" uri={current_track.uri}>
{current_track.name}
</URILink>
)}
{!stream_title && !current_track && (<span>-</span>)}
</div>
render = () => {
const {
current_track,
queue_tracks,
stream_title,
theme,
current_track_uri,
spotify_enabled,
uiActions,
mopidyActions: {
clearTracklist,
shuffleTracklist,
},
} = this.props;
let current_track_image = null;
if (current_track && current_track_uri) {
if (current_track.images !== undefined && current_track.images) {
current_track_image = current_track.images.large;
}
}
const options = (
<>
{spotify_enabled && (
<Button noHover discrete to="/queue/radio">
<Icon name="radio" />
<I18n path="now_playing.context_actions.radio" />
</Button>
)}
<Button noHover discrete to="/queue/history">
<Icon name="history" />
<I18n path="now_playing.context_actions.history" />
</Button>
<Button noHover discrete to="/queue/add-uri">
<Icon name="playlist_add" />
<I18n path="actions.add" />
</Button>
</>
);
return (
<div className="view queue-view preserve-3d">
<Header options={options} uiActions={uiActions}>
<Icon name="play_arrow" type="material" />
<I18n path="now_playing.title" />
</Header>
{theme === 'dark' && <Parallax image={current_track_image} blur />}
<div className="content-wrapper">
<div className="current-track">
<Artwork
image={current_track_image}
album_uri={current_track && current_track.album && current_track.album.uri}
<LinksSentence
className="current-track__artists"
items={current_track ? current_track.artists : null}
/>
<div className="current-track__details">
<div className="current-track__title">
{stream_title && (
<span>{stream_title}</span>
<AddedFrom
items={items}
added_from_uri={added_from_uri}
/>
<div className="current-track__queue-details">
<ul className="details">
<li>{`${queue_tracks.length} tracks`}</li>
<li><Dater type="total-time" data={queue_tracks} /></li>
{queue_tracks.length > 0 && (
<li>
<a onClick={shuffleTracklist}>
<Icon name="shuffle" />
<I18n path="now_playing.current_track.shuffle" />
</a>
</li>
)}
{!stream_title && current_track && (
<URILink type="track" uri={current_track.uri}>
{current_track.name}
</URILink>
{queue_tracks.length > 0 && (
<li>
<a onClick={clearTracklist}>
<Icon name="delete_sweep" />
<I18n path="now_playing.current_track.clear" />
</a>
</li>
)}
{!stream_title && !current_track && (<span>-</span>)}
</div>
<LinksSentence
className="current-track__artists"
items={current_track ? current_track.artists : null}
/>
{this.renderAddedFrom()}
<div className="current-track__queue-details">
<ul className="details">
<li>{`${queue_tracks.length} tracks`}</li>
<li><Dater type="total-time" data={queue_tracks} /></li>
{queue_tracks.length > 0 && (
<li>
<a onClick={shuffleTracklist}>
<Icon name="shuffle" />
<I18n path="now_playing.current_track.shuffle" />
</a>
</li>
)}
{queue_tracks.length > 0 && (
<li>
<a onClick={clearTracklist}>
<Icon name="delete_sweep" />
<I18n path="now_playing.current_track.clear" />
</a>
</li>
)}
</ul>
</div>
</ul>
</div>
</div>
<section className="list-wrapper">
<TrackList
uri="iris:queue"
show_source_icon
track_context="queue"
className="queue-track-list"
tracks={queue_tracks}
removeTracks={this.removeTracks}
playTracks={this.playTracks}
playTrack={this.playTrack}
reorderTracks={this.reorderTracks}
/>
</section>
</div>
<section className="list-wrapper">
<TrackList
uri="iris:queue"
show_source_icon
track_context="queue"
className="queue-track-list"
tracks={queue_tracks}
removeTracks={onRemoveTracks}
playTracks={onPlayTracks}
playTrack={onPlayTrack}
reorderTracks={onReorderTracks}
/>
</section>
</div>
);
}
</div>
);
}
const mapStateToProps = (state) => {