Files
Iris/src/js/components/TrackList.js

463 lines
13 KiB
JavaScript
Raw Normal View History

2016-10-03 14:44:27 +13:00
2019-01-15 16:57:50 +13:00
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
2016-10-14 18:15:34 +13:00
2019-01-15 16:57:50 +13:00
import Track from './Track';
import ContextMenuTrigger from './ContextMenuTrigger';
2016-10-03 14:44:27 +13:00
2019-01-15 16:57:50 +13:00
import * as helpers from '../helpers';
import * as mopidyActions from '../services/mopidy/actions';
import * as uiActions from '../services/ui/actions';
2016-11-10 16:17:10 +13:00
2016-10-14 18:15:34 +13:00
class TrackList extends React.Component{
2016-10-03 14:44:27 +13:00
2017-10-10 13:05:28 +13:00
constructor(props){
2019-01-15 16:57:50 +13:00
super(props);
2019-01-15 16:57:50 +13:00
this.touch_dragging_tracks_keys = false;
2019-01-15 16:57:50 +13:00
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleTouchMove = this.handleTouchMove.bind(this);
this.handleTouchEnd = this.handleTouchEnd.bind(this);
2016-10-27 13:02:10 +13:00
}
componentWillMount(){
2019-01-15 16:57:50 +13:00
window.addEventListener("keydown", this.handleKeyDown, false);
window.addEventListener("touchmove", this.handleTouchMove, false);
window.addEventListener("touchend", this.handleTouchEnd, false);
2016-10-27 13:02:10 +13:00
}
componentWillUnmount(){
2019-01-15 16:57:50 +13:00
window.removeEventListener("keydown", this.handleKeyDown, false);
window.removeEventListener("touchmove", this.handleTouchMove, false);
window.removeEventListener("touchend", this.handleTouchEnd, false);
2016-10-27 13:02:10 +13:00
}
handleKeyDown(e){
// When we're focussed on certian elements, don't fire any shortcuts
// Typically form inputs
let ignoreNodes = ['INPUT', 'TEXTAREA'];
if (ignoreNodes.indexOf(e.target.nodeName) > -1){
return;
}
// Ignore when there are any key modifiers. This enables us to avoid interfering
// with browser- and OS-default functions.
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey){
return;
}
var tracks_keys = this.digestTracksKeys()
let prevent = false;
switch(e.key.toLowerCase()){
case "enter":
if (tracks_keys && tracks_keys.length > 0){
this.playTracks();
}
prevent = true;
2016-10-27 13:02:10 +13:00
break;
case "d":
if (tracks_keys && tracks_keys.length > 0){
this.removeTracks();
}
prevent = true;
break;
case "a":
var all_tracks = []
for (var i = 0; i < this.props.tracks.length; i++){
all_tracks.push(this.buildTrackKey(this.props.tracks[i], i))
}
this.props.uiActions.setSelectedTracks(all_tracks);
prevent = true;
2016-10-27 13:02:10 +13:00
break;
}
if (prevent){
e.preventDefault();
return false;
}
2016-10-03 14:44:27 +13:00
}
handleDrag(e,track_key){
let selected_tracks = []
// Dragging a non-selected track. We need to deselect everything
// else and select only this track
if (!this.props.selected_tracks.includes(track_key)){
this.props.uiActions.setSelectedTracks([track_key])
selected_tracks = this.digestTracksKeys([track_key])
} else {
selected_tracks = this.digestTracksKeys()
}
let selected_tracks_indexes = helpers.arrayOf('index',selected_tracks)
this.props.uiActions.dragStart(
e,
2019-01-15 16:57:50 +13:00
this.props.track_context,
this.props.uri,
selected_tracks,
selected_tracks_indexes
)
}
handleDrop(e,track_key){
if (this.props.dragger && this.props.dragger.active){
// if this tracklist handles sorting, handle it
if (this.props.reorderTracks !== undefined){
let indexes = this.props.dragger.victims_indexes
let tracks = this.digestTracksKeys([track_key])
return this.props.reorderTracks(indexes, tracks[0].index)
}
}
this.touch_dragging_tracks_keys = false
}
handleTouchDrag(e,track_key){
let selected_tracks = []
// Drag initiated on a selected track
if (this.props.selected_tracks.includes(track_key)){
// They're all dragging
this.touch_dragging_tracks_keys = this.props.selected_tracks
// Not already selected
} else {
this.touch_dragging_tracks_keys = [track_key]
this.props.uiActions.setSelectedTracks([track_key])
}
}
handleTouchMove(e){
if (this.touch_dragging_tracks_keys){
let touch = e.touches[0];
let over = $(document.elementFromPoint(touch.clientX, touch.clientY));
if (!over.is('.track')){
2018-11-09 22:47:55 +13:00
over = over.closest('.list__item--track');
}
$(document).find('.touch-drag-hover').removeClass('touch-drag-hover');
if (over.length > 0){
over.addClass('touch-drag-hover');
}
e.returnValue = false;
e.cancelBubble = true;
e.preventDefault();
e.stopPropagation();
return false;
}
}
handleTouchEnd(e){
if (this.touch_dragging_tracks_keys){
let touch = e.changedTouches[0];
let over = $(document.elementFromPoint(touch.clientX, touch.clientY));
2018-11-09 22:47:55 +13:00
if (!over.is('.list__item--track')){
over = over.closest('.list__item--track');
}
if (over.length > 0){
2018-11-09 22:47:55 +13:00
let siblings = over.parent().children('.list__item--track');
let dropped_at = siblings.index(over);
if (this.props.reorderTracks !== undefined){
this.props.reorderTracks(helpers.arrayOf('index',this.digestTracksKeys()),dropped_at);
this.props.uiActions.setSelectedTracks([]);
}
}
$(document).find('.touch-drag-hover').removeClass('touch-drag-hover');
$('body').removeClass('touch-dragging');
}
this.touch_dragging_tracks_keys = false;
}
2017-12-20 16:33:38 +13:00
handleTap(e, track_key){
this.updateSelection(e, track_key, true);
}
handleDoubleTap(e,track_key){
this.playTracks([track_key]);
this.updateSelection(e, track_key);
}
handleClick(e, track_key){
this.updateSelection(e, track_key);
}
2017-07-21 22:56:28 +12:00
handleDoubleClick(e,track_key){
2017-12-06 17:02:11 +13:00
if (this.props.context_menu){
this.props.uiActions.hideContextMenu();
}
2017-12-20 16:33:38 +13:00
this.playTracks([track_key]);
this.updateSelection(e, track_key);
}
handleContextMenu(e, track_key = null){
2017-12-20 16:33:38 +13:00
// Do our best to stop any flow-on events
e.preventDefault();
e.stopPropagation();
e.cancelBubble = true;
let selected_tracks = this.props.selected_tracks;
2017-07-25 09:22:15 +12:00
// Not already selected, so select it prior to triggering menu
2017-07-26 15:24:18 +12:00
if (track_key && !selected_tracks.includes(track_key)){
selected_tracks = [track_key];
this.props.uiActions.setSelectedTracks(selected_tracks);
2017-07-25 09:22:15 +12:00
}
let selected_tracks_digested = this.digestTracksKeys(selected_tracks);
let selected_tracks_uris = helpers.arrayOf('uri',selected_tracks_digested);
let selected_tracks_indexes = helpers.arrayOf('index',selected_tracks_digested);
2017-05-18 09:17:55 +12:00
let data = {
e: e,
2019-01-15 16:57:50 +13:00
context: (this.props.track_context ? this.props.track_context+'-track' : 'track'),
2017-02-14 12:13:07 +13:00
tracklist_uri: (this.props.uri ? this.props.uri : null),
2017-07-25 09:22:15 +12:00
items: selected_tracks_digested,
uris: selected_tracks_uris,
indexes: selected_tracks_indexes
}
this.props.uiActions.showContextMenu(data);
}
2017-12-20 16:33:38 +13:00
updateSelection(e, track_key, touched = false){
let selected_tracks = this.props.selected_tracks;
2017-07-21 22:56:28 +12:00
if ((e.ctrlKey || e.metaKey) || touched){
2017-07-21 22:56:28 +12:00
// Already selected, so unselect it
if (selected_tracks.includes(track_key)){
var index = selected_tracks.indexOf(track_key);
selected_tracks.splice(index, 1);
2017-07-21 22:56:28 +12:00
// Not selected, so add it
} else {
selected_tracks.push(track_key);
}
2017-07-21 22:56:28 +12:00
} else if (e.shiftKey){
let last_selected_track = this.digestTracksKeys(selected_tracks[selected_tracks.length-1]);
let last_selected_track_index = last_selected_track.index;
let newly_selected_track = this.digestTracksKeys(track_key);
let newly_selected_track_index = newly_selected_track.index;
2017-07-21 22:56:28 +12:00
// We've selected a track further down the list,
// so proceed normally
if (last_selected_track_index < newly_selected_track_index){
var start = last_selected_track_index + 1;
var end = newly_selected_track_index;
2017-07-21 22:56:28 +12:00
// Selected a track up the list, so
// our last selected is the END of our range
} else {
var start = newly_selected_track_index;
var end = last_selected_track_index - 1;
2017-07-21 22:56:28 +12:00
}
2017-07-21 22:56:28 +12:00
if (start !== false && start >= 0 && end !== false && end >= 0){
for (let i = start; i <= end; i++){
selected_tracks.push(this.buildTrackKey(this.props.tracks[i], i));
2017-07-21 22:56:28 +12:00
}
}
2017-07-21 22:56:28 +12:00
// Regular, unmodified left click
} else {
selected_tracks = [track_key];
}
this.props.uiActions.setSelectedTracks(selected_tracks);
}
isRightClick(e){
if ('which' in e ){
return e.which == 3
} else if ('button' in e){
return e.button == 2;
}
return false;
2016-10-27 10:02:12 +13:00
}
2017-12-20 16:33:38 +13:00
playTracks(tracks_keys = null){
if (tracks_keys !== null){
var selected_tracks = this.digestTracksKeys(tracks_keys);
} else {
var selected_tracks = this.digestTracksKeys();
}
var selected_tracks_indexes = helpers.arrayOf('index', selected_tracks);
2017-12-06 17:02:11 +13:00
if (selected_tracks.length <= 0){
return this.props.uiActions.createNotification({content: 'No tracks selected', type: 'bad'});
2017-12-06 17:02:11 +13:00
}
// Our parent handles playing
if (this.props.playTracks !== undefined){
2017-12-06 17:02:11 +13:00
return this.props.playTracks(selected_tracks);
// Default to playing the URIs
} else {
2017-12-06 17:02:11 +13:00
let selected_tracks_uris = helpers.arrayOf('uri',selected_tracks);
return this.props.mopidyActions.playURIs(selected_tracks_uris, this.props.uri);
}
}
removeTracks(){
// Our parent handles removal
if (this.props.removeTracks !== undefined){
let selected_tracks = this.digestTracksKeys()
let selected_tracks_indexes = helpers.arrayOf('index',selected_tracks)
return this.props.removeTracks(selected_tracks_indexes)
}
// By default, do nothing
}
/**
* Build the track key
* This is our unique reference to a track in a particular tracklist
*
* @param track = Track obj
* @param index = int (position of track in tracklist)
* @return string
**/
buildTrackKey(track, index){
let key = index
key += '@@'+(track.tlid ? track.tlid : 'none')
key += '@@'+track.uri
key += '@@'+(this.props.uri ? this.props.uri : 'none')
2019-01-15 16:57:50 +13:00
key += '@@'+(this.props.track_context ? this.props.track_context : 'none')
return key
}
/**
* Digest our selected tracks
*
* @param tracks = mixed (defaults to stored value)
* @param indexex_only = boolean (do we just want an array of indexes)
* @return mixed
**/
digestTracksKeys(keys = this.props.selected_tracks, indexes_only = false){
if (!keys){
2017-07-21 22:56:28 +12:00
return false
}
// Accommodate a single key
var singleton = false
if (!(keys instanceof Array)){
singleton = true
keys = [keys]
}
2017-07-21 22:56:28 +12:00
// Construct a basic track object, based on our unique track key
// This is enough to perform interactions (dragging, selecting, etc)
var array = [];
for (var key of keys){
var key_components = key.split('@@')
2017-07-21 22:56:28 +12:00
if (indexes_only){
array.push(key_components[0])
2017-07-21 22:56:28 +12:00
} else {
array.push({
key: key,
index: parseInt(key_components[0]),
tlid: parseInt(key_components[1]),
uri: key_components[2],
context: key_components[3],
context_uri: key_components[4]
2017-07-21 22:56:28 +12:00
})
}
}
2017-07-21 22:56:28 +12:00
if (singleton && array.length > 0){
return array[0]
} else {
return array
}
2016-10-19 14:09:34 +13:00
}
2016-10-06 16:38:06 +13:00
render(){
2019-01-14 08:47:35 +13:00
if (!this.props.tracks || Object.prototype.toString.call(this.props.tracks) !== '[object Array]'){
2018-11-07 17:01:18 +13:00
return null;
}
2019-01-15 16:57:50 +13:00
var className = 'list list--tracks '+this.props.track_context
2017-03-13 13:17:11 +13:00
if (this.props.className){
className += ' '+this.props.className
}
return (
2017-03-13 13:17:11 +13:00
<div className={className}>
{
2017-07-21 22:56:28 +12:00
this.props.tracks.map(
(track, index) => {
2019-01-15 16:57:50 +13:00
var track_key = this.buildTrackKey(track, index);
track.key = track_key;
return (
<Track
show_source_icon={this.props.show_source_icon}
2017-07-21 22:56:28 +12:00
key={track_key}
mini_zones={this.props.slim_mode || helpers.isTouchDevice()}
track={track}
2019-01-15 16:57:50 +13:00
track_context={this.props.track_context}
can_sort={this.props.track_context == 'queue' || this.props.track_context == 'editable-playlist'}
2017-07-21 22:56:28 +12:00
selected={this.props.selected_tracks.includes(track_key)}
2018-11-09 08:33:16 +13:00
play_state={this.props.play_state}
dragger={this.props.dragger}
2017-12-20 16:33:38 +13:00
handleClick={(e) => this.handleClick(e, track_key)}
2017-07-21 22:56:28 +12:00
handleDoubleClick={e => this.handleDoubleClick(e, track_key)}
2017-07-25 09:22:15 +12:00
handleContextMenu={e => this.handleContextMenu(e, track_key)}
handleDrag={e => this.handleDrag(e, track_key)}
handleDrop={e => this.handleDrop(e, track_key)}
2017-12-20 16:33:38 +13:00
handleTap={e => this.handleTap(e, track_key)}
handleDoubleTap={e => this.handleDoubleTap(e, track_key)}
handleTouchDrag={e => this.handleTouchDrag(e, track_key)}
/>
)
}
)
}
2016-11-25 22:49:34 +13:00
</div>
);
2016-10-03 14:44:27 +13:00
}
2016-10-14 18:15:34 +13:00
}
const mapStateToProps = (state, ownProps) => {
return {
2018-11-09 08:33:16 +13:00
play_state: state.mopidy.play_state,
2017-08-23 07:39:33 +12:00
slim_mode: state.ui.slim_mode,
2017-07-21 22:56:28 +12:00
selected_tracks: state.ui.selected_tracks,
2016-11-15 17:08:21 +13:00
dragger: state.ui.dragger,
current_track: state.core.current_track,
context_menu: state.ui.context_menu
}
2016-10-14 18:15:34 +13:00
}
const mapDispatchToProps = (dispatch) => {
return {
2016-10-27 10:02:12 +13:00
mopidyActions: bindActionCreators(mopidyActions, dispatch),
uiActions: bindActionCreators(uiActions, dispatch)
2016-10-14 18:15:34 +13:00
}
}
2019-01-15 16:57:50 +13:00
export default connect(mapStateToProps, mapDispatchToProps)(TrackList)
2019-01-14 08:47:35 +13:00