Track now 100% functional component
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
34
mopidy_iris/static/app.min.js
vendored
34
mopidy_iris/static/app.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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
|
||||
|
||||
@ -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" />
|
||||
|
||||
</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" />
|
||||
|
||||
</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;
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user