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

417 lines
12 KiB
JavaScript
Raw Normal View History

2021-08-15 12:20:16 +12:00
import React, { useState, useEffect } from 'react';
2019-01-15 16:57:50 +13:00
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Track from './Track';
import * as mopidyActions from '../services/mopidy/actions';
import * as uiActions from '../services/ui/actions';
import { isTouchDevice } from '../util/helpers';
import { arrayOf } from '../util/arrays';
2020-11-07 16:46:09 +13:00
import { i18n } from '../locale';
import { SmartList } from './SmartList';
2016-11-10 16:17:10 +13:00
2021-08-15 12:20:16 +12:00
const TrackList = ({
uri,
track_context,
className = '',
show_source_icon,
play_state,
slim_mode,
tracks,
selected_tracks,
removeTracks,
playTracks,
reorderTracks,
context_menu,
dragger,
uiActions: {
createNotification,
setSelectedTracks,
showContextMenu,
dragStart,
},
mopidyActions: {
playURIs,
},
}) => {
const [touchDraggingKeys, setTouchDraggingKeys] = useState(false);
useEffect(() => {
window.addEventListener('keydown', onKeyDown, false);
window.addEventListener('touchmove', onTouchMove, false);
window.addEventListener('touchend', onTouchEnd, false);
return () => {
window.removeEventListener('keydown', onKeyDown, false);
window.removeEventListener('touchmove', onTouchMove, false);
window.removeEventListener('touchend', onTouchEnd, false);
}
}, [selected_tracks]);
2019-09-05 15:36:11 +12:00
2021-08-15 12:20:16 +12:00
const onKeyDown = (e) => {
const {
key,
altKey,
ctrlKey,
metaKey,
shiftKey,
} = e;
2019-09-05 15:36:11 +12:00
// When we're focussed on certian elements, don't fire any shortcuts
// Typically form inputs
const 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 (altKey || ctrlKey || metaKey || shiftKey) {
2019-09-05 15:36:11 +12:00
return;
}
2021-08-15 12:20:16 +12:00
const tracks_keys = digestTracksKeys();
2019-09-05 15:36:11 +12:00
switch (key.toLowerCase()) {
2019-09-05 15:36:11 +12:00
case 'enter':
if (tracks_keys && tracks_keys.length > 0) {
2021-08-15 12:20:16 +12:00
onPlayTracks();
}
2020-09-11 13:53:54 +12:00
e.preventDefault();
2019-09-05 15:36:11 +12:00
break;
2019-09-05 15:36:11 +12:00
case 'backspace':
case 'delete':
if (tracks_keys && tracks_keys.length > 0) {
2021-08-15 12:20:16 +12:00
onRemoveTracks();
}
2020-09-11 13:53:54 +12:00
e.preventDefault();
2019-09-05 15:36:11 +12:00
break;
2019-09-05 15:36:11 +12:00
case 'a':
var all_tracks = [];
for (let i = 0; i < this.props.tracks.length; i++) {
all_tracks.push(this.buildTrackKey(this.props.tracks[i], i));
}
2019-09-05 15:36:11 +12:00
this.props.uiActions.setSelectedTracks(all_tracks);
2020-09-11 13:53:54 +12:00
e.preventDefault();
2019-09-05 15:36:11 +12:00
break;
}
}
2021-08-15 12:20:16 +12:00
const onDrag = (e, track_key) => {
let nextSelectedTracks = [];
2019-09-05 15:36:11 +12:00
// Dragging a non-selected track. We need to deselect everything
// else and select only this track
2021-08-15 12:20:16 +12:00
if (!nextSelectedTracks.includes(track_key)) {
setSelectedTracks([track_key]);
nextSelectedTracks = digestTracksKeys([track_key]);
2019-09-05 15:36:11 +12:00
} else {
2021-08-15 12:20:16 +12:00
nextSelectedTracks = digestTracksKeys();
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
const nextSelectedTracks_indexes = arrayOf('index', nextSelectedTracks);
2019-09-05 15:36:11 +12:00
2021-08-15 12:20:16 +12:00
dragStart(
2019-09-05 15:36:11 +12:00
e,
2021-08-15 12:20:16 +12:00
track_context,
uri,
nextSelectedTracks,
nextSelectedTracks_indexes,
2019-09-05 15:36:11 +12:00
);
}
2021-08-15 12:20:16 +12:00
const onDrop = (e, track_key) => {
if (dragger?.active && reorderTracks) {
const tracks = digestTracksKeys([track_key]);
return reorderTracks(dragger?.victims_indexes, tracks[0].index);
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
setTouchDraggingKeys(false);
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
const onTouchDrag = (e, track_key) => {
2019-09-05 15:36:11 +12:00
// Drag initiated on a selected track
2021-08-15 12:20:16 +12:00
if (selected_tracks.includes(track_key)) {
2019-09-05 15:36:11 +12:00
// They're all dragging
2021-08-15 12:20:16 +12:00
setTouchDraggingKeys(selected_tracks);
2019-09-05 15:36:11 +12:00
// Not already selected, so just dragging the one track
} else {
2021-08-15 12:20:16 +12:00
setTouchDraggingKeys([track_key]);
setSelectedTracks([track_key]);
2019-09-05 15:36:11 +12:00
}
}
2021-08-15 12:20:16 +12:00
const onTouchMove = (e) => {
if (touchDraggingKeys) {
2019-09-05 15:36:11 +12:00
const touch = e.touches[0];
let over = $(document.elementFromPoint(touch.clientX, touch.clientY));
if (!over.is('.track')) {
over = over.closest('.list__item--track');
}
$(document).find('.touch-drag-hover').removeClass('touch-drag-hover');
if (over.length > 0) {
over.addClass('touch-drag-hover');
}
2021-08-15 12:20:16 +12:00
e.returnValue = false;
e.cancelBubble = true;
2019-09-05 15:36:11 +12:00
e.preventDefault();
e.stopPropagation();
2021-08-15 12:20:16 +12:00
return false;
2019-09-05 15:36:11 +12:00
}
}
2021-08-15 12:20:16 +12:00
const onTouchEnd = (e) => {
if (touchDraggingKeys) {
2019-09-05 15:36:11 +12:00
const touch = e.changedTouches[0];
let over = $(document.elementFromPoint(touch.clientX, touch.clientY));
if (!over.is('.list__item--track')) {
over = over.closest('.list__item--track');
}
if (over.length > 0) {
const siblings = over.parent().children('.list__item--track');
const dropped_at = siblings.index(over);
2021-08-15 12:20:16 +12:00
if (reorderTracks !== undefined) {
reorderTracks(arrayOf('index', digestTracksKeys()), dropped_at);
setSelectedTracks([]);
2019-09-05 15:36:11 +12:00
}
}
$(document).find('.touch-drag-hover').removeClass('touch-drag-hover');
$('body').removeClass('touch-dragging');
2021-08-15 12:20:16 +12:00
setTouchDraggingKeys(false);
2019-09-05 15:36:11 +12:00
}
}
2021-08-15 12:20:16 +12:00
const onTap = (e, track_key) => {
updateSelection(e, track_key, true);
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
const onDoubleTap = (e, track_key) => {
onPlayTracks([track_key]);
updateSelection(e, track_key);
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
const onClick = (e, track_key) => {
updateSelection(e, track_key);
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
const onDoubleClick = (e, track_key) => {
if (context_menu) hideContextMenu();
onPlayTracks([track_key]);
updateSelection(e, track_key);
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
const onContextMenu = (e, track_key = null) => {
2019-09-05 15:36:11 +12:00
// Do our best to stop any flow-on events
e.preventDefault();
e.stopPropagation();
e.cancelBubble = true;
2021-08-15 12:20:16 +12:00
let nextSelectedTracks = [...selected_tracks];
2019-09-05 15:36:11 +12:00
// Not already selected, so select it prior to triggering menu
2021-08-15 12:20:16 +12:00
if (track_key && !nextSelectedTracks.includes(track_key)) {
nextSelectedTracks = [track_key];
setSelectedTracks(nextSelectedTracks);
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
const selected_tracks_digested = digestTracksKeys(nextSelectedTracks);
const selected_tracks_uris = arrayOf('uri', selected_tracks_digested);
const selected_tracks_indexes = arrayOf('index', selected_tracks_digested);
2019-09-05 15:36:11 +12:00
2021-08-15 12:20:16 +12:00
showContextMenu({
2019-09-05 15:36:11 +12:00
e,
2021-08-15 12:20:16 +12:00
context: (track_context ? `${track_context}-track` : 'track'),
tracklist_uri: uri,
2019-09-05 15:36:11 +12:00
items: selected_tracks_digested,
uris: selected_tracks_uris,
indexes: selected_tracks_indexes,
2021-08-15 12:20:16 +12:00
});
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
const updateSelection = (e, track_key, touched = false) => {
let nextSelectedTracks = [...selected_tracks];
2019-09-05 15:36:11 +12:00
if ((e.ctrlKey || e.metaKey) || touched) {
2019-09-05 15:36:11 +12:00
// Already selected, so unselect it
2021-08-15 12:20:16 +12:00
if (nextSelectedTracks.includes(track_key)) {
const index = nextSelectedTracks.indexOf(track_key);
nextSelectedTracks.splice(index, 1);
2019-09-05 15:36:11 +12:00
// Not selected, so add it
} else {
2021-08-15 12:20:16 +12:00
nextSelectedTracks.push(track_key);
}
} else if (e.shiftKey) {
2021-08-15 12:20:16 +12:00
const last_selected_track = digestTracksKeys(nextSelectedTracks[nextSelectedTracks.length - 1]);
2019-09-05 15:36:11 +12:00
const last_selected_track_index = last_selected_track.index;
2021-08-15 12:20:16 +12:00
const newly_selected_track = digestTracksKeys(track_key);
2019-09-05 15:36:11 +12:00
const newly_selected_track_index = newly_selected_track.index;
// 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;
// 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;
}
if (start !== false && start >= 0 && end !== false && end >= 0) {
for (let i = start; i <= end; i++) {
2021-08-15 12:20:16 +12:00
nextSelectedTracks.push(buildTrackKey(tracks[i], i));
2019-09-05 15:36:11 +12:00
}
}
// Regular, unmodified left click
} else {
2021-08-15 12:20:16 +12:00
nextSelectedTracks = [track_key];
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
setSelectedTracks(nextSelectedTracks);
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
const onPlayTracks = (tracks_keys = null) => {
const selectedKeys = tracks_keys !== null
? digestTracksKeys(tracks_keys)
: digestTracksKeys();
2019-09-05 15:36:11 +12:00
2021-08-15 12:20:16 +12:00
if (selectedKeys.length <= 0) {
2020-11-07 16:46:09 +13:00
createNotification({ content: i18n('errors.nothing_selected'), level: 'error' });
return;
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
if (playTracks) {
playTracks(selectedKeys);
2020-11-07 16:46:09 +13:00
return;
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
playURIs(arrayOf('uri', selectedKeys), uri);
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
const onRemoveTracks = () => {
const selectedKeys = digestTracksKeys();
2019-09-05 15:36:11 +12:00
2021-08-15 12:20:16 +12:00
if (!removeTracks) {
createNotification({ content: `Cannot delete ${selectedKeys.length > 1 ? 'these tracks' : 'this track'}`, level: 'error' });
2020-11-07 16:46:09 +13:00
return;
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
const selected_tracks_indexes = arrayOf('index', selectedKeys);
removeTracks(selected_tracks_indexes);
2019-09-05 15:36:11 +12:00
}
/**
* 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
2019-09-05 15:36:11 +12:00
* */
2021-08-15 12:20:16 +12:00
const buildTrackKey = (track, index) => {
2019-09-05 15:36:11 +12:00
let key = index;
2021-08-15 12:20:16 +12:00
key += `@@${track.tlid || 'none'}`;
2019-09-05 15:36:11 +12:00
key += `@@${track.uri}`;
2021-08-15 12:20:16 +12:00
key += `@@${uri || 'none'}`;
key += `@@${track_context || 'none'}`;
2019-09-05 15:36:11 +12:00
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
2019-09-05 15:36:11 +12:00
* */
2021-08-15 12:20:16 +12:00
const digestTracksKeys = (keys = selected_tracks, indexes_only = false) => {
2019-09-05 15:36:11 +12:00
if (!keys) {
return false;
}
// Accommodate a single key
let singleton = false;
if (!(keys instanceof Array)) {
singleton = true;
keys = [keys];
}
// Construct a basic track object, based on our unique track key
// This is enough to perform interactions (dragging, selecting, etc)
const array = [];
for (const key of keys) {
const key_components = key.split('@@');
if (indexes_only) {
array.push(key_components[0]);
2019-09-05 15:36:11 +12:00
} else {
array.push({
key,
index: parseInt(key_components[0]),
tlid: parseInt(key_components[1]),
uri: key_components[2],
context: key_components[3],
context_uri: key_components[4],
});
}
}
if (singleton && array.length > 0) {
return array[0];
}
return array;
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
if (!tracks || Object.prototype.toString.call(tracks) !== '[object Array]') {
return null;
2019-09-05 15:36:11 +12:00
}
2021-08-15 12:20:16 +12:00
return (
<SmartList
className={`list list--tracks ${track_context} ${className}`}
items={tracks}
itemComponent={Track}
itemProps={{
dragger,
buildTrackKey,
play_state,
show_source_icon,
track_context,
selected_tracks,
can_sort: track_context === 'queue' || track_context === 'editable-playlist',
mini_zones: slim_mode || isTouchDevice(),
handleClick: onClick,
handleDoubleClick: onDoubleClick,
handleContextMenu: onContextMenu,
handleDrag: onDrag,
handleDrop: onDrop,
handleTap: onTap,
handleDoubleTap: onDoubleTap,
handleTouchDrag: onTouchDrag,
}}
/>
);
2016-10-14 18:15:34 +13:00
}
2020-11-07 16:46:09 +13:00
const mapStateToProps = (state) => ({
2019-09-05 15:36:11 +12:00
play_state: state.mopidy.play_state,
slim_mode: state.ui.slim_mode,
selected_tracks: state.ui.selected_tracks,
dragger: state.ui.dragger,
current_track: state.core.current_track,
context_menu: state.ui.context_menu,
2020-08-20 00:36:23 -04:00
stream_title: state.core.stream_title,
2019-09-05 15:36:11 +12:00
});
const mapDispatchToProps = (dispatch) => ({
mopidyActions: bindActionCreators(mopidyActions, dispatch),
uiActions: bindActionCreators(uiActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(TrackList);