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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -115,7 +115,7 @@
// Release details
// These are automatically injected to built HTML
var build = "1628389192";
var build = "1628970566";
var version = "3.58.0";
// Construct the script tag

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