import React, { PropTypes } from 'react' import { connect } from 'react-redux' import { bindActionCreators } from 'redux' import FontAwesome from 'react-fontawesome' import Track from './Track' import ContextMenuTrigger from './ContextMenuTrigger' import * as helpers from '../helpers' import * as mopidyActions from '../services/mopidy/actions' import * as uiActions from '../services/ui/actions' class TrackList extends React.Component{ constructor(props) { super(props) this.touch_dragging_tracks_keys = false this.handleKeyUp = this.handleKeyUp.bind(this) this.handleTouchMove = this.handleTouchMove.bind(this) this.handleTouchEnd = this.handleTouchEnd.bind(this) } componentWillMount(){ window.addEventListener("keyup", this.handleKeyUp, false) window.addEventListener("touchmove", this.handleTouchMove, false) window.addEventListener("touchend", this.handleTouchEnd, false) } componentWillUnmount(){ window.removeEventListener("keyup", this.handleKeyUp, false) window.removeEventListener("touchmove", this.handleTouchMove, false) window.removeEventListener("touchend", this.handleTouchEnd, false) } /** * Figure out if our click/touch event is valid and we can act accordingly * * @param e = event obj * @return string **/ triggerType(e){ var target = $(e.target) // Wide screen, so no worries if (!this.props.slim_mode){ return 'default' } else if (target.is('.select-zone') || target.closest('.select-zone').length > 0){ return 'mobile' } return false } /** * 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.uri key += '_'+(this.props.uri ? this.props.uri : 'none') key += '_'+(this.props.context ? this.props.context : 'none') return key } handleKeyUp(e){ if (!this.digestTracksKeys()) return switch(e.keyCode){ case 13: // enter this.playTracks(); break; case 46: // delete this.removeTracks(); break; } } 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, this.props.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')){ over = over.closest('.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)) if (!over.is('.track')){ over = over.closest('.track') } if (over.length > 0){ let siblings = over.parent().children('.track') let dropped_at = siblings.index(over) - 1 console.log('touchend',helpers.arrayOf('index',this.digestTracksKeys()),'to',dropped_at) 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 } handleDoubleClick(e,track_key){ if (this.props.context_menu) this.props.uiActions.hideContextMenu() this.playTracks() } handleContextMenu(e,track_key){ let selected_tracks = this.props.selected_tracks // Not already selected, so select it prior to triggering menu if (!selected_tracks.includes(track_key)){ selected_tracks = [track_key] this.props.uiActions.setSelectedTracks(selected_tracks) } 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) let data = { e: e, context: (this.props.context ? this.props.context+'-track' : 'track'), tracklist_uri: (this.props.uri ? this.props.uri : null), items: selected_tracks_digested, uris: selected_tracks_uris, indexes: selected_tracks_indexes } this.props.uiActions.showContextMenu(data) } handleSelection(e,track_key){ let selected_tracks = this.props.selected_tracks if (e.ctrlKey || this.props.slim_mode){ // Already selected, so unselect it if (selected_tracks.includes(track_key)){ var index = selected_tracks.indexOf(track_key) selected_tracks.splice(index,1) // Not selected, so add it } else { selected_tracks.push(track_key) } } else if (e.shiftKey){ let last_selected_track_index = selected_tracks[selected_tracks.length-1].split('_')[0] let newly_selected_track_index = track_key.split('_')[0] // 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 var end = newly_selected_track_index // 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 } 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)) } } // 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 if( 'button' in e ) return e.button == 2 return false } /** * Digest our selected tracks * * @param tracks = array (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 || keys.length <= 0){ return false } // Construct a basic track object, based on our unique track key // This is enough to perform interactions (dragging, selecting, etc) let array = [] for (let i = 0; i < keys.length; i++){ let key = keys[i].split('_') if (indexes_only){ array.push(key[0]) } else { array.push({ index: parseInt(key[0]), uri: key[1], context: key[2], context_uri: key[3] }) } } return array } playTracks(){ let selected_tracks = this.digestTracksKeys() let selected_tracks_indexes = helpers.arrayOf('index',selected_tracks) // Our parent handles playing if (this.props.playTracks !== undefined){ return this.props.playTracks( selected_tracks ) // Default to playing the URIs } else { 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 } renderHeader(){ if( this.props.noheader ) return null switch (this.props.context){ case 'history': return (