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

247 lines
6.8 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';
2021-12-28 20:55:09 +13:00
import { uniqBy } from 'lodash';
2019-01-15 16:57:50 +13:00
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';
import { SmartList } from './SmartList';
2016-11-10 16:17:10 +13:00
2021-08-15 12:20:16 +12:00
const TrackList = ({
2021-12-05 21:07:21 +13:00
context,
2021-08-15 12:20:16 +12:00
className = '',
show_source_icon,
play_state,
slim_mode,
tracks,
playTracks,
2021-08-15 12:20:16 +12:00
removeTracks,
reorderTracks,
uiActions: {
createNotification,
showContextMenu,
},
mopidyActions: {
playURIs,
changeTrack,
2021-08-15 12:20:16 +12:00
},
}) => {
const [selected, setSelected] = useState([]);
useEffect(() => {
setSelected([]);
}, [tracks.length]);
2021-08-15 12:20:16 +12:00
useEffect(() => {
window.addEventListener('keydown', onKeyDown, false);
return () => {
window.removeEventListener('keydown', onKeyDown, false);
2021-12-28 20:55:09 +13:00
};
}, [selected]);
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;
}
switch (key.toLowerCase()) {
2019-09-05 15:36:11 +12:00
case 'enter':
onPlayTracks();
2020-09-11 13:53:54 +12:00
e.preventDefault();
2019-09-05 15:36:11 +12:00
break;
case 'backspace':
case 'delete':
onRemoveTracks();
2020-09-11 13:53:54 +12:00
e.preventDefault();
2019-09-05 15:36:11 +12:00
break;
case 'a':
setSelected(tracks.map((item, index) => ({ item, index })));
2020-09-11 13:53:54 +12:00
e.preventDefault();
2019-09-05 15:36:11 +12:00
break;
default:
break;
2019-09-05 15:36:11 +12:00
}
}
2021-12-28 20:55:09 +13:00
const events = {
onDrop: (index) => {
2021-12-28 20:55:09 +13:00
reorderTracks(arrayOf('index', selected), index);
setSelected([]);
},
onMouseDown: (item, index, e) => {
if (selectionIndexByItemIndex(index) === -1) {
e.persist();
setSelected((prev) => nextSelected(prev, item, index, e));
}
},
onTouchStart: (item, index, e) => { // Only bound to the drag handler
e.persist();
setSelected((prev) => nextSelected(prev, item, index, e));
},
2021-12-28 20:55:09 +13:00
onDoubleClick: (item, index) => {
setSelected([{ item, index }]);
if (context?.type === 'queue') {
changeTrack(item.tlid);
} else {
playURIs({ uris: [item.uri], from: context });
}
2021-12-28 20:55:09 +13:00
},
onContextMenu: (item, index, e) => {
// Do our best to stop any flow-on events
e.preventDefault();
e.stopPropagation();
e.cancelBubble = true;
const items = getOrUpdateSelected(item, index, e).map(
({ index, item }) => ({ index, ...item }),
);
2021-12-28 20:55:09 +13:00
showContextMenu({
e,
context,
...(items.length === 1
? { type: 'track', item: items[0] }
: { type: 'tracks', items }
),
});
},
};
2019-09-05 15:36:11 +12:00
const onPlayTracks = () => {
if (!selected || !selected.length) return;
const selectedTracks = arrayOf('item', selected);
if (playTracks) {
playTracks(selectedTracks);
} else {
playURIs({ uris: arrayOf('uri', selectedTracks), from: context });
}
};
2021-12-28 20:55:09 +13:00
const onRemoveTracks = () => {
if (!selected || !selected.length) return;
2021-12-28 20:55:09 +13:00
if (!removeTracks) {
createNotification({
content: `Cannot delete ${selected.length > 1 ? 'these tracks' : 'this track'}`,
level: 'error',
});
return;
}
removeTracks(selected.map(({ index }) => index));
};
2019-09-05 15:36:11 +12:00
2021-12-28 20:55:09 +13:00
const selectionIndexByItemIndex = (index) => (
selected && selected.length > 0
? selected.findIndex(({ index: selectedIndex }) => selectedIndex === index)
: -1
);
2019-09-05 15:36:11 +12:00
2021-12-28 20:55:09 +13:00
// Based on a single track's event, procure the next selected array and update the state
// Target not selected = select it
// Target selected = leave as-is and return current selection
const getOrUpdateSelected = (item, index, e) => {
const alreadySelected = selectionIndexByItemIndex(index);
let items = [];
if (alreadySelected > -1) {
items = selected;
} else {
items = nextSelected(selected, item, index, e);
setSelected(items);
2019-09-05 15:36:11 +12:00
}
2021-12-28 20:55:09 +13:00
return items;
}
2019-09-05 15:36:11 +12:00
const nextSelected = (prev, item, index, e = {}, sticky = false) => {
2021-12-28 20:55:09 +13:00
const alreadySelected = selectionIndexByItemIndex(index);
2019-09-05 15:36:11 +12:00
2021-12-28 20:55:09 +13:00
if (e.shiftKey) {
const { index: lastSelectedIndex } = selected[selected.length-1] || { index };
const next = [...prev, { index, item }];
const from = lastSelectedIndex < index ? lastSelectedIndex : index;
const to = lastSelectedIndex > index ? lastSelectedIndex : index;
for (let i = from; i < to; i++) {
next.push({ index: i, item: tracks[i] });
}
return uniqBy(next, 'index');
2019-09-05 15:36:11 +12:00
}
2021-12-28 20:55:09 +13:00
if (e.ctrlKey) {
if (alreadySelected >= 0 && !sticky) {
const next = [...prev];
next.splice(alreadySelected, 1);
return next;
}
return [...prev, { item, index }];
}
return [{ item, index }];
};
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
2021-12-28 20:55:09 +13:00
const is_selected = (index) => selected.find(({ index: i }) => index === i);
const getDragItem = (item, index) => {
let selectedForDrag = selected;
if (selectionIndexByItemIndex(index) === -1) {
selectedForDrag = nextSelected(selected, item, index);
}
return {
items: selectedForDrag,
context,
};
};
2021-08-15 12:20:16 +12:00
return (
<SmartList
2021-12-05 21:07:21 +13:00
className={`list list--tracks ${className}`}
2021-08-15 12:20:16 +12:00
items={tracks}
itemComponent={Track}
itemProps={{
play_state,
show_source_icon,
context,
getDragItem,
can_sort: context?.can_edit,
is_selected,
2021-08-15 12:20:16 +12:00
mini_zones: slim_mode || isTouchDevice(),
2021-12-28 20:55:09 +13:00
events,
2021-08-15 12:20:16 +12:00
}}
/>
);
2021-12-28 20:55:09 +13:00
};
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,
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);