Refactoring selection, now TrackList state-managed
This commit is contained in:
@ -6,7 +6,7 @@ import * as Sentry from '@sentry/browser';
|
||||
|
||||
import Sidebar from './components/Sidebar';
|
||||
import PlaybackControls from './components/PlaybackControls';
|
||||
import ContextMenu from './components/ContextMenu';
|
||||
import ContextMenu from './components/ContextMenu/ContextMenu';
|
||||
import Dragger from './components/Dragger';
|
||||
import Notifications from './components/Notifications';
|
||||
import ResizeListener from './components/ResizeListener';
|
||||
|
||||
@ -11,38 +11,92 @@ import {
|
||||
isLoading,
|
||||
throttle,
|
||||
titleCase,
|
||||
} from '../util/helpers';
|
||||
} from '../../util/helpers';
|
||||
import {
|
||||
arrayOf,
|
||||
sortItems,
|
||||
} from '../util/arrays';
|
||||
import Link from './Link';
|
||||
import Icon from './Icon';
|
||||
import Loader from './Loader';
|
||||
import URILink from './URILink';
|
||||
} from '../../util/arrays';
|
||||
import Link from '../Link';
|
||||
import Icon from '../Icon';
|
||||
import Loader from '../Loader';
|
||||
import URILink from '../URILink';
|
||||
|
||||
import * as coreActions from '../services/core/actions';
|
||||
import * as uiActions from '../services/ui/actions';
|
||||
import * as pusherActions from '../services/pusher/actions';
|
||||
import * as mopidyActions from '../services/mopidy/actions';
|
||||
import * as lastfmActions from '../services/lastfm/actions';
|
||||
import * as spotifyActions from '../services/spotify/actions';
|
||||
import { I18n } from '../locale';
|
||||
import { encodeUri } from '../util/format';
|
||||
import * as coreActions from '../../services/core/actions';
|
||||
import * as uiActions from '../../services/ui/actions';
|
||||
import * as pusherActions from '../../services/pusher/actions';
|
||||
import * as mopidyActions from '../../services/mopidy/actions';
|
||||
import * as lastfmActions from '../../services/lastfm/actions';
|
||||
import * as spotifyActions from '../../services/spotify/actions';
|
||||
import { I18n } from '../../locale';
|
||||
import { encodeUri } from '../../util/format';
|
||||
import {
|
||||
makeProcessProgressSelector,
|
||||
makeProvidersSelector,
|
||||
makeLibrarySelector,
|
||||
} from '../util/selectors';
|
||||
import AddedFrom from './AddedFrom';
|
||||
import { ContextMenuItems } from './ContextMenu/ContextMenuItems';
|
||||
import PlaylistSubmenu from './ContextMenu/PlaylistSubmenu';
|
||||
} from '../../util/selectors';
|
||||
import AddedFrom from '../AddedFrom';
|
||||
import { ContextMenuItems } from './ContextMenuItems';
|
||||
import PlaylistSubmenu from './PlaylistSubmenu';
|
||||
|
||||
const processKeys = [
|
||||
'MOPIDY_GET_LIBRARY_PLAYLISTS',
|
||||
'SPOTIFY_GET_LIBRARY_PLAYLISTS',
|
||||
];
|
||||
|
||||
const Title = ({
|
||||
context_menu: {
|
||||
title,
|
||||
item,
|
||||
items,
|
||||
type,
|
||||
},
|
||||
}) => {
|
||||
if (items) {
|
||||
return (
|
||||
<div className="context-menu__title">
|
||||
<div className="context-menu__title__text">
|
||||
{`${items.length} ${type} selected`}
|
||||
<span
|
||||
className="context-menu__title__deselect"
|
||||
onClick={() => {
|
||||
// TODO: reset selected
|
||||
}}
|
||||
>
|
||||
<Icon name="close" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (item) {
|
||||
return (
|
||||
<div className="context-menu__title">
|
||||
<AddedFrom
|
||||
from={item?.added_from}
|
||||
by={item?.added_by}
|
||||
className="context-menu__title__text"
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'custom') {
|
||||
if (!title) return null;
|
||||
|
||||
return (
|
||||
<div className="context-menu__title">
|
||||
<div className="context-menu__title__text">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const ContextMenu = ({
|
||||
context_menu,
|
||||
loading_progress, // to move to submenu
|
||||
@ -105,7 +159,9 @@ const ContextMenu = ({
|
||||
return (
|
||||
<div className={className} style={style} ref={ref}>
|
||||
<div className="context-menu__section context-menu__section--items">
|
||||
{/* {this.renderTitle()} */}
|
||||
<Title
|
||||
context_menu={context_menu}
|
||||
/>
|
||||
<ContextMenuItems
|
||||
context_menu={context_menu}
|
||||
onSubmenu={() => setSubmenu(true)}
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { memo } from 'react';
|
||||
import Icon from './Icon';
|
||||
import Button from './Button';
|
||||
import { trackEvent } from './Trackable';
|
||||
import Icon from '../Icon';
|
||||
import Button from '../Button';
|
||||
import { trackEvent } from '../Trackable';
|
||||
|
||||
export default memo(({ onTrigger }) => {
|
||||
const handleClick = (e) => {
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { memo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import ContextMenuTrigger from './ContextMenuTrigger';
|
||||
import ContextMenuTrigger from './ContextMenu/ContextMenuTrigger';
|
||||
import { showContextMenu } from '../services/ui/actions';
|
||||
|
||||
export default memo(({
|
||||
|
||||
@ -5,7 +5,7 @@ import LinksSentence from './LinksSentence';
|
||||
import { dater } from './Dater';
|
||||
import { nice_number } from './NiceNumber';
|
||||
import URILink from './URILink';
|
||||
import ContextMenuTrigger from './ContextMenuTrigger';
|
||||
import ContextMenuTrigger from './ContextMenu/ContextMenuTrigger';
|
||||
import Icon, { SourceIcon } from './Icon';
|
||||
import Thumbnail from './Thumbnail';
|
||||
import Popularity from './Popularity';
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import Icon, { SourceIcon } from './Icon';
|
||||
import LinksSentence from './LinksSentence';
|
||||
import { Dater, dater } from './Dater';
|
||||
import URILink from './URILink';
|
||||
import ContextMenuTrigger from './ContextMenuTrigger';
|
||||
import ContextMenuTrigger from './ContextMenu/ContextMenuTrigger';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
import { isTouchDevice } from '../util/helpers';
|
||||
import { I18n, i18n } from '../locale';
|
||||
@ -18,7 +18,7 @@ const MiddleColumn = ({
|
||||
} = {},
|
||||
}) => {
|
||||
let content;
|
||||
switch (context?.context) {
|
||||
switch (context?.type) {
|
||||
case 'history': {
|
||||
content = (
|
||||
<div className="list__item__column__item list__item__column__item--played_at">
|
||||
@ -58,169 +58,172 @@ const Track = ({
|
||||
stream_title,
|
||||
play_state,
|
||||
selected_tracks,
|
||||
is_selected,
|
||||
can_sort,
|
||||
show_source_icon,
|
||||
getItemIndex,
|
||||
handleContextMenu,
|
||||
handleDrag,
|
||||
handleDrop,
|
||||
handleClick,
|
||||
handleDoubleClick,
|
||||
handleDoubleTap,
|
||||
handleTouchDrag,
|
||||
handleTap,
|
||||
dragger,
|
||||
buildTrackKey,
|
||||
onContextMenu,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
}) => {
|
||||
const [hover, setHover] = useState(false);
|
||||
const key = buildTrackKey(item, getItemIndex());
|
||||
const index = getItemIndex();
|
||||
const [currentEvent, setCurrentEvent] = useState({
|
||||
start_time: 0,
|
||||
end_time: 0,
|
||||
});
|
||||
|
||||
const onMouseEnter = () => setHover(true);
|
||||
const onMouseLeave = () => setHover(false);
|
||||
const onContextMenu = (e) => handleContextMenu(e, key);
|
||||
const onDoubleClick = (e) => handleDoubleClick(e, key);
|
||||
const updateCurrentEvent = (data) => setCurrentEvent((prev) => ({ ...prev, ...data }));
|
||||
// const onMouseEnter = () => setHover(true);
|
||||
// const onMouseLeave = () => setHover(false);
|
||||
// const onDoubleClick = (e) => handleDoubleClick(e, key);
|
||||
// const updateCurrentEvent = (data) => setCurrentEvent((prev) => ({ ...prev, ...data }));
|
||||
|
||||
const onMouseDown = (e) => {
|
||||
const target = $(e.target);
|
||||
const handleContextMenu = (e) => onContextMenu(item, index, e);
|
||||
const handleClick = (e) => onClick(item, index, e);
|
||||
const handleDoubleClick = (e) => onDoubleClick(item, index, e);
|
||||
|
||||
// Clicked a nested link (ie Artist name), so no dragging required
|
||||
if (target.is('a')) {
|
||||
return false;
|
||||
}
|
||||
// const onMouseDown = (e) => {
|
||||
// const target = $(e.target);
|
||||
|
||||
// Only listen for left mouse clicks
|
||||
if (e.button === 0) {
|
||||
updateCurrentEvent({
|
||||
start_position: {
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
},
|
||||
});
|
||||
// // Clicked a nested link (ie Artist name), so no dragging required
|
||||
// if (target.is('a')) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// Not left click, then ensure no dragging
|
||||
} else {
|
||||
updateCurrentEvent({ start_position: false });
|
||||
}
|
||||
};
|
||||
// // Only listen for left mouse clicks
|
||||
// if (e.button === 0) {
|
||||
// updateCurrentEvent({
|
||||
// start_position: {
|
||||
// x: e.pageX,
|
||||
// y: e.pageY,
|
||||
// },
|
||||
// });
|
||||
|
||||
const onMouseMove = (e) => {
|
||||
if (handleDrag === undefined) return false;
|
||||
// // Not left click, then ensure no dragging
|
||||
// } else {
|
||||
// updateCurrentEvent({ start_position: false });
|
||||
// }
|
||||
// };
|
||||
|
||||
if (currentEvent.start_position) {
|
||||
const start_x = currentEvent.start_position.x;
|
||||
const start_y = currentEvent.start_position.y;
|
||||
const threshold = 5;
|
||||
// const onMouseMove = (e) => {
|
||||
// if (handleDrag === undefined) return false;
|
||||
|
||||
// Have we dragged outside of our threshold zone?
|
||||
if (
|
||||
e.pageX > start_x + threshold
|
||||
|| e.pageX < start_x - threshold
|
||||
|| e.pageY > start_y + threshold
|
||||
|| e.pageY < start_y - threshold
|
||||
) {
|
||||
// Handover to parent for dragging. We can unset all our behaviour now.
|
||||
handleDrag(e, key);
|
||||
updateCurrentEvent({ start_position: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
// if (currentEvent.start_position) {
|
||||
// const start_x = currentEvent.start_position.x;
|
||||
// const start_y = currentEvent.start_position.y;
|
||||
// const threshold = 5;
|
||||
|
||||
const onMouseUp = (e) => {
|
||||
const target = $(e.target);
|
||||
// // Have we dragged outside of our threshold zone?
|
||||
// if (
|
||||
// e.pageX > start_x + threshold
|
||||
// || e.pageX < start_x - threshold
|
||||
// || e.pageY > start_y + threshold
|
||||
// || e.pageY < start_y - threshold
|
||||
// ) {
|
||||
// // Handover to parent for dragging. We can unset all our behaviour now.
|
||||
// handleDrag(e, key);
|
||||
// updateCurrentEvent({ start_position: false });
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
// Only listen for left clicks
|
||||
if (e.button === 0) {
|
||||
if (dragger) {
|
||||
e.preventDefault();
|
||||
// const onMouseUp = (e) => {
|
||||
// const target = $(e.target);
|
||||
|
||||
if (handleDrop !== undefined) {
|
||||
handleDrop(e, key);
|
||||
}
|
||||
} else if (!target.is('a') && target.closest('a').length <= 0) {
|
||||
handleClick(e, key);
|
||||
updateCurrentEvent({ start_position: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
// // Only listen for left clicks
|
||||
// if (e.button === 0) {
|
||||
// if (dragger) {
|
||||
// e.preventDefault();
|
||||
|
||||
// Not left click, then ensure no dragging
|
||||
updateCurrentEvent({ start_position: false });
|
||||
}
|
||||
// if (handleDrop !== undefined) {
|
||||
// handleDrop(e, key);
|
||||
// }
|
||||
// } else if (!target.is('a') && target.closest('a').length <= 0) {
|
||||
// handleClick(e, key);
|
||||
// updateCurrentEvent({ start_position: false });
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Not left click, then ensure no dragging
|
||||
// updateCurrentEvent({ start_position: false });
|
||||
// }
|
||||
|
||||
|
||||
const onTouchStart = (e) => {
|
||||
const target = $(e.target);
|
||||
const timestamp = Math.floor(Date.now());
|
||||
// const onTouchStart = (e) => {
|
||||
// const target = $(e.target);
|
||||
// const timestamp = Math.floor(Date.now());
|
||||
|
||||
if (target.hasClass('drag-zone')) {
|
||||
handleTouchDrag(e, key);
|
||||
e.preventDefault();
|
||||
}
|
||||
// if (target.hasClass('drag-zone')) {
|
||||
// handleTouchDrag(e, key);
|
||||
// e.preventDefault();
|
||||
// }
|
||||
|
||||
// Save touch start details
|
||||
updateCurrentEvent({
|
||||
start_time: timestamp,
|
||||
start_position: {
|
||||
x: e.touches[0].clientX,
|
||||
y: e.touches[0].clientY,
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
// // Save touch start details
|
||||
// updateCurrentEvent({
|
||||
// start_time: timestamp,
|
||||
// start_position: {
|
||||
// x: e.touches[0].clientX,
|
||||
// y: e.touches[0].clientY,
|
||||
// },
|
||||
// });
|
||||
// return false;
|
||||
// }
|
||||
|
||||
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 end_position = {
|
||||
x: e.changedTouches[0].clientX,
|
||||
y: e.changedTouches[0].clientY,
|
||||
};
|
||||
// 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 end_position = {
|
||||
// x: e.changedTouches[0].clientX,
|
||||
// y: e.changedTouches[0].clientY,
|
||||
// };
|
||||
|
||||
// Too long between touchstart and touchend
|
||||
if (currentEvent.start_time + tap_time_threshold < timestamp) {
|
||||
return false;
|
||||
}
|
||||
// // Too long between touchstart and touchend
|
||||
// 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 (
|
||||
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')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
// // Make sure there's enough distance between start and end before we handle
|
||||
// // this event as a 'tap'
|
||||
// if (
|
||||
// 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')) {
|
||||
// e.preventDefault();
|
||||
// }
|
||||
|
||||
// Context trigger
|
||||
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, key);
|
||||
return false;
|
||||
}
|
||||
// // Context trigger
|
||||
// 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, key);
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// We received a touchend within 300ms ago, so handle as double-tap
|
||||
if ((timestamp - currentEvent.end_time) > 0 && (timestamp - currentEvent.end_time) <= 300) {
|
||||
handleDoubleTap(e, key);
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
// // We received a touchend within 300ms ago, so handle as double-tap
|
||||
// if ((timestamp - currentEvent.end_time) > 0 && (timestamp - currentEvent.end_time) <= 300) {
|
||||
// handleDoubleTap(e, key);
|
||||
// e.preventDefault();
|
||||
// return false;
|
||||
// }
|
||||
|
||||
handleTap(e, key);
|
||||
}
|
||||
// handleTap(e, key);
|
||||
// }
|
||||
|
||||
updateCurrentEvent({ end_time: timestamp });
|
||||
}
|
||||
// updateCurrentEvent({ end_time: timestamp });
|
||||
// }
|
||||
|
||||
if (!item) return null;
|
||||
|
||||
@ -268,7 +271,7 @@ const Track = ({
|
||||
}
|
||||
|
||||
const track_middle_column = <MiddleColumn context={context} item={item} />;
|
||||
if (selected_tracks.includes(key)) className += ' list__item--selected';
|
||||
if (is_selected(getItemIndex())) 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';
|
||||
@ -282,15 +285,18 @@ const Track = ({
|
||||
<ErrorBoundary>
|
||||
<div
|
||||
className={className}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseMove={onMouseMove}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onContextMenu={onContextMenu}
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchEnd={onTouchEnd}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
// 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">
|
||||
|
||||
@ -5,7 +5,7 @@ import Track from './Track';
|
||||
import * as mopidyActions from '../services/mopidy/actions';
|
||||
import * as uiActions from '../services/ui/actions';
|
||||
import { isTouchDevice, uriSource } from '../util/helpers';
|
||||
import { arrayOf } from '../util/arrays';
|
||||
import { arrayOf, indexToArray } from '../util/arrays';
|
||||
import { i18n } from '../locale';
|
||||
import { SmartList } from './SmartList';
|
||||
import { formatSimpleObject } from '../util/format';
|
||||
@ -34,19 +34,50 @@ const TrackList = ({
|
||||
playURIs,
|
||||
},
|
||||
}) => {
|
||||
const [touchDraggingKeys, setTouchDraggingKeys] = useState(false);
|
||||
const [selected, setSelected] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', onKeyDown, false);
|
||||
window.addEventListener('touchmove', onTouchMove, false);
|
||||
window.addEventListener('touchend', onTouchEnd, 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);
|
||||
// window.removeEventListener('touchmove', onTouchMove, false);
|
||||
// window.removeEventListener('touchend', onTouchEnd, false);
|
||||
}
|
||||
}, [selected_tracks]);
|
||||
|
||||
const selectionIndexByItemIndex = (index) => (
|
||||
selected && selected.length > 0
|
||||
? selected.findIndex(({ index: selectedIndex }) => selectedIndex === index)
|
||||
: -1
|
||||
);
|
||||
|
||||
const nextSelected = (prev, item, index, e, sticky = false) => {
|
||||
const alreadySelected = selectionIndexByItemIndex(index);
|
||||
|
||||
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 next;
|
||||
}
|
||||
if (e.ctrlKey) {
|
||||
if (alreadySelected >= 0 && !sticky) {
|
||||
const next = [...prev];
|
||||
next.splice(alreadySelected, 1);
|
||||
return next;
|
||||
}
|
||||
return [...prev, { item, index }];
|
||||
}
|
||||
return [{ item, index }];
|
||||
}
|
||||
|
||||
const onKeyDown = (e) => {
|
||||
const {
|
||||
key,
|
||||
@ -69,156 +100,58 @@ const TrackList = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const tracks_keys = digestTracksKeys();
|
||||
|
||||
switch (key.toLowerCase()) {
|
||||
case 'enter':
|
||||
if (tracks_keys && tracks_keys.length > 0) {
|
||||
if (selected.length > 0) {
|
||||
onPlayTracks();
|
||||
}
|
||||
e.preventDefault();
|
||||
break;
|
||||
|
||||
case 'backspace':
|
||||
case 'delete':
|
||||
if (tracks_keys && tracks_keys.length > 0) {
|
||||
if (selected.length > 0) {
|
||||
onRemoveTracks();
|
||||
}
|
||||
e.preventDefault();
|
||||
break;
|
||||
|
||||
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));
|
||||
}
|
||||
this.props.uiActions.setSelectedTracks(all_tracks);
|
||||
setSelected(tracks.map((item, index) => ({ item, index })));
|
||||
e.preventDefault();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const onDrag = (e, track_key) => {
|
||||
let nextSelectedTracks = [];
|
||||
const onClick = (item, index, e) => {
|
||||
e.preventDefault();
|
||||
e.persist();
|
||||
|
||||
// Dragging a non-selected track. We need to deselect everything
|
||||
// else and select only this track
|
||||
if (!selected_tracks.includes(track_key)) {
|
||||
setSelectedTracks([track_key]);
|
||||
nextSelectedTracks = digestTracksKeys([track_key]);
|
||||
} else {
|
||||
nextSelectedTracks = digestTracksKeys();
|
||||
}
|
||||
setSelected((prev) => nextSelected(prev, item, index, e));
|
||||
};
|
||||
|
||||
const nextSelectedTracks_indexes = arrayOf('index', nextSelectedTracks);
|
||||
const onDoubleClick = (item, index) => {
|
||||
// if (context_menu) hideContextMenu();
|
||||
setSelected([{ item, index }]);
|
||||
playURIs([item.uri], context);
|
||||
};
|
||||
|
||||
dragStart(
|
||||
e,
|
||||
context?.type,
|
||||
uri,
|
||||
nextSelectedTracks,
|
||||
nextSelectedTracks_indexes,
|
||||
);
|
||||
}
|
||||
|
||||
const onDrop = (e, track_key) => {
|
||||
if (dragger?.active && reorderTracks) {
|
||||
const tracks = digestTracksKeys([track_key]);
|
||||
return reorderTracks(dragger?.victims_indexes, tracks[0].index);
|
||||
}
|
||||
setTouchDraggingKeys(false);
|
||||
}
|
||||
|
||||
const onTouchDrag = (e, track_key) => {
|
||||
// Drag initiated on a selected track
|
||||
if (selected_tracks.includes(track_key)) {
|
||||
// They're all dragging
|
||||
setTouchDraggingKeys(selected_tracks);
|
||||
|
||||
// Not already selected, so just dragging the one track
|
||||
} else {
|
||||
setTouchDraggingKeys([track_key]);
|
||||
setSelectedTracks([track_key]);
|
||||
}
|
||||
}
|
||||
|
||||
const onTouchMove = (e) => {
|
||||
if (touchDraggingKeys) {
|
||||
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');
|
||||
}
|
||||
|
||||
e.returnValue = false;
|
||||
e.cancelBubble = true;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const onTouchEnd = (e) => {
|
||||
if (touchDraggingKeys) {
|
||||
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);
|
||||
|
||||
if (reorderTracks !== undefined) {
|
||||
reorderTracks(arrayOf('index', digestTracksKeys()), dropped_at);
|
||||
setSelectedTracks([]);
|
||||
}
|
||||
}
|
||||
|
||||
$(document).find('.touch-drag-hover').removeClass('touch-drag-hover');
|
||||
$('body').removeClass('touch-dragging');
|
||||
setTouchDraggingKeys(false);
|
||||
}
|
||||
}
|
||||
|
||||
const onTap = (e, track_key) => {
|
||||
updateSelection(e, track_key, true);
|
||||
}
|
||||
|
||||
const onDoubleTap = (e, track_key) => {
|
||||
onPlayTracks([track_key]);
|
||||
updateSelection(e, track_key);
|
||||
}
|
||||
|
||||
const onClick = (e, track_key) => {
|
||||
updateSelection(e, track_key);
|
||||
}
|
||||
|
||||
const onDoubleClick = (e, track_key) => {
|
||||
if (context_menu) hideContextMenu();
|
||||
onPlayTracks([track_key]);
|
||||
updateSelection(e, track_key);
|
||||
}
|
||||
|
||||
const onContextMenu = (e, track_key = null) => {
|
||||
const onContextMenu = (item, index, e) => {
|
||||
// Do our best to stop any flow-on events
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.cancelBubble = true;
|
||||
|
||||
let nextSelectedTracks = [...selected_tracks];
|
||||
|
||||
// Not already selected, so select it prior to triggering menu
|
||||
if (track_key && !nextSelectedTracks.includes(track_key)) {
|
||||
nextSelectedTracks = [track_key];
|
||||
setSelectedTracks(nextSelectedTracks);
|
||||
const alreadySelected = selectionIndexByItemIndex(index);
|
||||
let items = [];
|
||||
if (alreadySelected > -1) {
|
||||
items = selected;
|
||||
} else {
|
||||
items = nextSelected(selected, item, index, e);
|
||||
setSelected(items);
|
||||
}
|
||||
items = items.map(({ item: selectedItem }) => selectedItem);
|
||||
|
||||
const items = digestTracksKeys(nextSelectedTracks);
|
||||
showContextMenu({
|
||||
e,
|
||||
context,
|
||||
@ -227,98 +160,19 @@ const TrackList = ({
|
||||
: { type: 'tracks', items }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const updateSelection = (e, track_key, touched = false) => {
|
||||
let nextSelectedTracks = [...selected_tracks];
|
||||
|
||||
if ((e.ctrlKey || e.metaKey) || touched) {
|
||||
// Already selected, so unselect it
|
||||
if (nextSelectedTracks.includes(track_key)) {
|
||||
const index = nextSelectedTracks.indexOf(track_key);
|
||||
nextSelectedTracks.splice(index, 1);
|
||||
|
||||
// Not selected, so add it
|
||||
} else {
|
||||
nextSelectedTracks.push(track_key);
|
||||
}
|
||||
} else if (e.shiftKey) {
|
||||
const last_selected_track = digestTracksKeys(nextSelectedTracks[nextSelectedTracks.length - 1]);
|
||||
const last_selected_track_index = last_selected_track.index;
|
||||
const newly_selected_track = digestTracksKeys(track_key);
|
||||
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++) {
|
||||
nextSelectedTracks.push(buildTrackKey(tracks[i], i));
|
||||
}
|
||||
}
|
||||
|
||||
// Regular, unmodified left click
|
||||
} else {
|
||||
nextSelectedTracks = [track_key];
|
||||
}
|
||||
|
||||
setSelectedTracks(nextSelectedTracks);
|
||||
}
|
||||
|
||||
const onPlayTracks = (tracks_keys = null) => {
|
||||
const selectedKeys = tracks_keys !== null
|
||||
? digestTracksKeys(tracks_keys)
|
||||
: digestTracksKeys();
|
||||
|
||||
if (selectedKeys.length <= 0) {
|
||||
createNotification({ content: i18n('errors.nothing_selected'), level: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (playTracks) {
|
||||
playTracks(selectedKeys);
|
||||
return;
|
||||
}
|
||||
playURIs(arrayOf('uri', selectedKeys), uri);
|
||||
}
|
||||
};
|
||||
|
||||
const onRemoveTracks = () => {
|
||||
const selectedKeys = digestTracksKeys();
|
||||
|
||||
if (!removeTracks) {
|
||||
createNotification({ content: `Cannot delete ${selectedKeys.length > 1 ? 'these tracks' : 'this track'}`, level: 'error' });
|
||||
createNotification({
|
||||
content: `Cannot delete ${selected.length > 1 ? 'these tracks' : 'this track'}`,
|
||||
level: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const selected_tracks_indexes = arrayOf('index', selectedKeys);
|
||||
removeTracks(selected_tracks_indexes);
|
||||
}
|
||||
removeTracks(selected.map(({ index }) => index));
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* */
|
||||
const buildTrackKey = (track, index) => {
|
||||
let key = index;
|
||||
key += `@@${track.tlid || 'none'}`;
|
||||
key += `@@${track.uri}`;
|
||||
key += `@@${uri || 'none'}`;
|
||||
key += `@@${context?.type || 'none'}`;
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Digest our selected tracks
|
||||
@ -327,49 +181,54 @@ const TrackList = ({
|
||||
* @param indexex_only = boolean (do we just want an array of indexes)
|
||||
* @return mixed
|
||||
* */
|
||||
const digestTracksKeys = (keys = selected_tracks, indexes_only = false) => {
|
||||
if (!keys) {
|
||||
return false;
|
||||
}
|
||||
// const digestTracksKeys = (keys = selected_tracks, indexes_only = false) => {
|
||||
// if (!keys) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// Accommodate a single key
|
||||
let singleton = false;
|
||||
if (!(keys instanceof Array)) {
|
||||
singleton = true;
|
||||
keys = [keys];
|
||||
}
|
||||
// // 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('@@');
|
||||
// // 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]);
|
||||
} else {
|
||||
array.push({
|
||||
key,
|
||||
index: parseInt(key_components[0]),
|
||||
tlid: parseInt(key_components[1]),
|
||||
uri: key_components[2],
|
||||
provider: uriSource(key_components[2]),
|
||||
context: key_components[3],
|
||||
context_uri: key_components[4],
|
||||
});
|
||||
}
|
||||
}
|
||||
// if (indexes_only) {
|
||||
// array.push(key_components[0]);
|
||||
// } else {
|
||||
// array.push({
|
||||
// key,
|
||||
// index: parseInt(key_components[0]),
|
||||
// tlid: parseInt(key_components[1]),
|
||||
// uri: key_components[2],
|
||||
// provider: uriSource(key_components[2]),
|
||||
// context: key_components[3],
|
||||
// context_uri: key_components[4],
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
if (singleton && array.length > 0) {
|
||||
return array[0];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
// if (singleton && array.length > 0) {
|
||||
// return array[0];
|
||||
// }
|
||||
// return array;
|
||||
// }
|
||||
|
||||
if (!tracks || Object.prototype.toString.call(tracks) !== '[object Array]') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const is_selected = (index) => {
|
||||
if (selected.length <= 0) return false;
|
||||
return selected.filter(({ index: selIndex }) => index === selIndex).length > 0;
|
||||
};
|
||||
|
||||
return (
|
||||
<SmartList
|
||||
className={`list list--tracks ${className}`}
|
||||
@ -377,23 +236,16 @@ const TrackList = ({
|
||||
itemComponent={Track}
|
||||
itemProps={{
|
||||
dragger,
|
||||
buildTrackKey,
|
||||
play_state,
|
||||
show_source_icon,
|
||||
context,
|
||||
selected_tracks,
|
||||
can_sort: (
|
||||
context?.context === 'queue' || (context?.context === 'playlist' && context?.can_edit)
|
||||
),
|
||||
can_sort: context?.can_edit,
|
||||
is_selected,
|
||||
mini_zones: slim_mode || isTouchDevice(),
|
||||
handleClick: onClick,
|
||||
handleDoubleClick: onDoubleClick,
|
||||
handleContextMenu: onContextMenu,
|
||||
handleDrag: onDrag,
|
||||
handleDrop: onDrop,
|
||||
handleTap: onTap,
|
||||
handleDoubleTap: onDoubleTap,
|
||||
handleTouchDrag: onTouchDrag,
|
||||
onClick,
|
||||
onContextMenu,
|
||||
onDoubleClick,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -10,7 +10,7 @@ import Loader from '../components/Loader';
|
||||
import FollowButton from '../components/Fields/FollowButton';
|
||||
import { nice_number } from '../components/NiceNumber';
|
||||
import { Dater } from '../components/Dater';
|
||||
import ContextMenuTrigger from '../components/ContextMenuTrigger';
|
||||
import ContextMenuTrigger from '../components/ContextMenu/ContextMenuTrigger';
|
||||
import { SourceIcon } from '../components/Icon';
|
||||
import DropdownField from '../components/Fields/DropdownField';
|
||||
import FilterField from '../components/Fields/FilterField';
|
||||
|
||||
@ -7,7 +7,7 @@ import Link from '../../components/Link';
|
||||
import Thumbnail from '../../components/Thumbnail';
|
||||
import Parallax from '../../components/Parallax';
|
||||
import FollowButton from '../../components/Fields/FollowButton';
|
||||
import ContextMenuTrigger from '../../components/ContextMenuTrigger';
|
||||
import ContextMenuTrigger from '../../components/ContextMenu/ContextMenuTrigger';
|
||||
import About from './About';
|
||||
import Overview from './Overview';
|
||||
import Tracks from './Tracks';
|
||||
|
||||
@ -10,7 +10,7 @@ import Parallax from '../../components/Parallax';
|
||||
import DropdownField from '../../components/Fields/DropdownField';
|
||||
import AddSeedField from '../../components/Fields/AddSeedField';
|
||||
import URILink from '../../components/URILink';
|
||||
import ContextMenuTrigger from '../../components/ContextMenuTrigger';
|
||||
import ContextMenuTrigger from '../../components/ContextMenu/ContextMenuTrigger';
|
||||
import RelatedArtists from '../../components/RelatedArtists';
|
||||
import Icon from '../../components/Icon';
|
||||
import * as uiActions from '../../services/ui/actions';
|
||||
|
||||
@ -11,7 +11,7 @@ import { nice_number } from '../components/NiceNumber';
|
||||
import { Dater, dater } from '../components/Dater';
|
||||
import FollowButton from '../components/Fields/FollowButton';
|
||||
import Loader from '../components/Loader';
|
||||
import ContextMenuTrigger from '../components/ContextMenuTrigger';
|
||||
import ContextMenuTrigger from '../components/ContextMenu/ContextMenuTrigger';
|
||||
import URILink from '../components/URILink';
|
||||
import { SourceIcon } from '../components/Icon';
|
||||
import DropdownField from '../components/Fields/DropdownField';
|
||||
|
||||
@ -9,7 +9,7 @@ import LinksSentence from '../components/LinksSentence';
|
||||
import LastfmLoveButton from '../components/Fields/LastfmLoveButton';
|
||||
import { Dater } from '../components/Dater';
|
||||
import SelectField from '../components/Fields/SelectField';
|
||||
import ContextMenuTrigger from '../components/ContextMenuTrigger';
|
||||
import ContextMenuTrigger from '../components/ContextMenu/ContextMenuTrigger';
|
||||
import Icon from '../components/Icon';
|
||||
import Loader from '../components/Loader';
|
||||
import * as coreActions from '../services/core/actions';
|
||||
|
||||
@ -21,7 +21,7 @@ import {
|
||||
makeItemSelector,
|
||||
makeLoadingSelector,
|
||||
} from '../util/selectors';
|
||||
import ContextMenuTrigger from '../components/ContextMenuTrigger';
|
||||
import ContextMenuTrigger from '../components/ContextMenu/ContextMenuTrigger';
|
||||
|
||||
class User extends React.Component {
|
||||
componentDidMount() {
|
||||
|
||||
Reference in New Issue
Block a user