React DnD as provider working pretty well, still WIP

This commit is contained in:
James Barnsley
2022-01-10 21:17:54 +13:00
parent 83c15278e9
commit 4fbae32458
17 changed files with 9741 additions and 468 deletions

View File

@ -2,6 +2,8 @@ import React, { useState, useEffect } from 'react';
import { Route, Switch, useLocation, useHistory } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import ReactGA from 'react-ga';
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend';
import * as Sentry from '@sentry/browser';
import Sidebar from './components/Sidebar';
@ -252,76 +254,78 @@ const App = () => {
onClick={handleInteraction}
onKeyDown={handleInteraction}
>
<div className="body">
<Switch>
<Route
path="/modal"
component={Modals}
/>
<Route>
<div>
<Sidebar tabIndex="3" />
<PlaybackControls tabIndex="2"/>
<DndProvider backend={HTML5Backend}>
<div className="body">
<Switch>
<Route
path="/modal"
component={Modals}
/>
<Route>
<div>
<Sidebar tabIndex="3" />
<PlaybackControls tabIndex="2"/>
<main id="main" className="smooth-scroll" tabIndex="1">
<Switch>
<Route exact path="/" component={Queue} />
<main id="main" className="smooth-scroll" tabIndex="1">
<Switch>
<Route exact path="/" component={Queue} />
<Route exact path="/queue" component={Queue} />
<Route
exact
path="/queue/history"
component={QueueHistory}
/>
<Route exact path="/settings/debug" component={Debug} />
<Route path="/settings" component={Settings} />
<Route exact path="/queue" component={Queue} />
<Route
exact
path="/queue/history"
component={QueueHistory}
/>
<Route exact path="/settings/debug" component={Debug} />
<Route path="/settings" component={Settings} />
<Route
exact
path="/search/:type?/:term?"
component={Search}
/>
<Route
exact
path="/artist/:uri/:sub_view?"
component={Artist}
/>
<Route exact path="/album/:uri/:name?" component={Album} />
<Route exact path="/playlist/:uri/:name?" component={Playlist} />
<Route exact path="/user/:uri/:name?" component={User} />
<Route exact path="/track/:uri/:name?" component={Track} />
<Route exact path="/uri/:uri/:name?" component={UriRedirect} />
<Route
exact
path="/search/:type?/:term?"
component={Search}
/>
<Route
exact
path="/artist/:uri/:sub_view?"
component={Artist}
/>
<Route exact path="/album/:uri/:name?" component={Album} />
<Route exact path="/playlist/:uri/:name?" component={Playlist} />
<Route exact path="/user/:uri/:name?" component={User} />
<Route exact path="/track/:uri/:name?" component={Track} />
<Route exact path="/uri/:uri/:name?" component={UriRedirect} />
<Route
path="/discover"
component={Discover}
/>
<Route
path="/discover"
component={Discover}
/>
<Route
path="/library"
component={Library}
/>
<Route
path="/library"
component={Library}
/>
<Route>
<ErrorMessage type="not-found" title="Not found">
<p>Oops, that link could not be found</p>
</ErrorMessage>
</Route>
</Switch>
</main>
</div>
</Route>
</Switch>
</div>
<Route>
<ErrorMessage type="not-found" title="Not found">
<p>Oops, that link could not be found</p>
</ErrorMessage>
</Route>
</Switch>
</main>
</div>
</Route>
</Switch>
</div>
<ResizeListener />
{hotkeys_enabled && <Hotkeys />}
<ContextMenu />
<Dragger />
<Notifications />
{hasInteracted && <ErrorBoundary silent><Stream /></ErrorBoundary>}
{hasInteracted && ('mediaSession' in navigator) && <MediaSession />}
{debug_info && <DebugInfo />}
<ResizeListener />
{hotkeys_enabled && <Hotkeys />}
<ContextMenu />
<Dragger />
<Notifications />
{hasInteracted && <ErrorBoundary silent><Stream /></ErrorBoundary>}
{hasInteracted && ('mediaSession' in navigator) && <MediaSession />}
{debug_info && <DebugInfo />}
</DndProvider>
</div>
);
};

View File

@ -33,7 +33,7 @@ class Dragger extends React.Component {
handleMouseMove(e) {
const { dragger, uiActions: { dragActive } } = this.props;
const { target } = e;
console.debug('mouseMove', e)
// console.debug('mouseMove', e)
if (!dragger) return null;
const threshold = 10;

View File

@ -1,6 +1,7 @@
import React, { useState } from 'react';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { useDragLayer, useDrop } from 'react-dnd';
import * as mopidyActions from '../../services/mopidy/actions';
import Icon from '../Icon';
import { arrayOf } from '../../util/arrays';
@ -33,64 +34,65 @@ const zones = [
];
const Dropzones = () => {
const dispatch = useDispatch();
const history = useHistory();
const [dropTarget, setDropTarget] = useState();
const {
victims,
from_uri,
active,
} = useSelector((state) => state.ui.dragger || {});
if (!active) return null;
const { isDragging } = useDragLayer((monitor) => ({
isDragging: monitor.isDragging(),
}));
const onDragEnter = (e, action) => {
e.preventDefault();
e.stopPropagation();
setDropTarget(action);
}
const onDragOver = (e, action) => {
e.preventDefault();
e.stopPropagation();
setDropTarget(action);
}
const onDrop = (e, action) => {
e.preventDefault();
e.stopPropagation();
const uris = arrayOf('uri', victims);
switch (action) {
case 'enqueue':
dispatch(mopidyActions.enqueueURIs(uris, from_uri));
break;
case 'enqueue_next':
dispatch(mopidyActions.enqueueURIs(uris, from_uri, true));
break;
case 'add_to_playlist':
history.push(`/modal/add-to-playlist/${encodeUri(uris.join(','))}`);
break;
case 'create_playlist_and_add':
history.push(`/modal/create-playlist/${encodeUri(uris.join(','))}`);
break;
default:
break;
}
};
if (!isDragging) return null;
return (
<div className="dropzones">
{zones.map(({ title, icon, action }) => (
<div
key={action}
className={`dropzones__item ${dropTarget === action ? ' hover' : ''}`}
onDragEnter={(e) => onDragEnter(e, action)}
onDragOver={(e) => onDragOver(e, action)}
onDrop={(e) => onDrop(e, action)}
>
<Icon name={icon} />
<span className="title">{title}</span>
</div>
))}
{zones.map((zone) => <Dropzone key={zone.action} {...zone} />)}
</div>
);
};
const Dropzone = ({
title,
icon,
action,
}) => {
const dispatch = useDispatch();
const history = useHistory();
const [{ handlerId, isOver }, drop] = useDrop({
accept: 'TRACK',
collect: (monitor) => ({
handlerId: monitor.getHandlerId(),
isOver: monitor.isOver(),
}),
drop: ({ selected, context }) => {
const uris = selected.map(({ item: { uri } }) => uri);
console.debug({ selected, context, uris })
switch (action) {
case 'enqueue':
dispatch(mopidyActions.enqueueURIs(uris, context));
break;
case 'enqueue_next':
dispatch(mopidyActions.enqueueURIs(uris, context, true));
break;
case 'add_to_playlist':
history.push(`/modal/add-to-playlist/${encodeUri(uris.join(','))}`);
break;
case 'create_playlist_and_add':
history.push(`/modal/create-playlist/${encodeUri(uris.join(','))}`);
break;
default:
break;
}
},
});
return (
<div
ref={drop}
data-handler-id={handlerId}
className={`dropzones__item ${isOver ? 'dropzones__item--drag-over' : ''}`}
// onDragEnter={(e) => onDragEnter(e, action)}
// onDragOver={(e) => onDragOver(e, action)}
// onDrop={(e) => onDrop(e, action)}
>
<Icon name={icon} />
<span className="title">{title}</span>
</div>
);
};

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react';
import React, { useRef } from 'react';
import { useDrag, useDrop } from 'react-dnd';
import Icon, { SourceIcon } from './Icon';
import LinksSentence from './LinksSentence';
import { Dater, dater } from './Dater';
@ -18,7 +19,7 @@ const MiddleColumn = ({
} = {},
}) => {
let content;
switch (context?.type) {
switch (context ?.type) {
case 'history': {
content = (
<div className="list__item__column__item list__item__column__item--played_at">
@ -58,13 +59,33 @@ const Track = ({
stream_title,
play_state,
is_selected,
// is_dropping,
can_sort,
show_source_icon,
getItemIndex,
getDragItem,
events,
}) => {
const index = getItemIndex();
const ref = useRef(null);
const [{ handlerId, isOver }, drop] = useDrop({
accept: 'TRACK',
collect: (monitor) => ({
handlerId: monitor.getHandlerId(),
isOver: can_sort && monitor.isOver(),
}),
canDrop: () => can_sort,
drop: () => {
events.onDrop(index);
},
});
const [{ isDragging }, drag] = useDrag({
type: 'TRACK',
item: getDragItem(item, index),
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drag(drop(ref));
if (!item) return null;
@ -113,7 +134,8 @@ const Track = ({
const track_middle_column = <MiddleColumn context={context} item={item} />;
if (is_selected(index)) className += ' list__item--selected';
// if (is_dropping(index)) className += ' list__item--dropping';
if (isOver) className += ' list__item--drag-over';
if (isDragging) className += ' list__item--dragging';
if (can_sort) className += ' list__item--can-sort';
if (item.type !== undefined) className += ` list__item--${item.type}`;
if (item.playing) className += ' list__item--playing';
@ -123,27 +145,19 @@ const Track = ({
if (track_details.length > 0) className += ' list__item--has-details';
const onClick = (e) => events.onClick(item, index, e);
const onMouseDown = (e) => events.onMouseDown(item, index, e);
const onDoubleClick = (e) => events.onDoubleClick(item, index, e);
const onContextMenu = (e) => events.onContextMenu(item, index, e);
const onDragStart = (e) => events.onDragStart(item, index, e);
const onDragEnd = (e) => events.onDragEnd(item, index, e);
const onDragOver = (e) => events.onDragOver(item, index, e);
const onDragEnter = (e) => events.onDragEnter(item, index, e);
const onDrop = (e) => events.onDrop(item, index, e);
return (
<ErrorBoundary>
<div
className={`${className} dropzone`}
onClick={onClick}
className={className}
onMouseDown={onMouseDown}
onDoubleClick={onDoubleClick}
onContextMenu={onContextMenu}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onDragOver={onDragOver}
onDragEnter={onDragEnter}
onDrop={onDrop}
draggable="true"
ref={ref}
data-handler-id={handlerId}
>
<div className="list__item__column list__item__column--name">
<div className="list__item__column__item--name">
@ -161,7 +175,7 @@ const Track = ({
{drag_zone}
{item.is_explicit && <span className="flag flag--dark">{i18n('track.explicit').toUpperCase()}</span>}
{item.is_playable === false && <span className="flag flag--dark">{i18n('track.unplayable').toUpperCase()}</span>}
{(context?.type === 'album' || context?.type === 'artist') && item.track_number && (
{(context ?.type === 'album' || context ?.type === 'artist') && item.track_number && (
<span className="mid_grey-text list__item__column__item list__item__column__item--track-number">
<span>
<I18n path="track.title" />

View File

@ -40,10 +40,8 @@ const TrackList = ({
useEffect(() => {
window.addEventListener('keydown', onKeyDown, false);
window.addEventListener('dragend', onDragEnd, false);
return () => {
window.removeEventListener('keydown', onKeyDown, false);
window.removeEventListener('dragend', onDragEnd, false);
};
}, []);
@ -97,28 +95,21 @@ const TrackList = ({
const items = getOrUpdateSelected(item, index, e).map(({ item: selectedItem }) => selectedItem);
dragStart(e, context, context, items, selected);
},
onDrop: (item, index, e) => {
onDrop: (index) => {
reorderTracks(arrayOf('index', selected), index);
setSelected([]);
},
onDragEnd: (item, index, e) => {
dragEnd();
},
onDragEnter: (item, index, e) => {
e.stopPropagation();
e.preventDefault();
console.debug('onDragEnter', index);
setDropTarget(index);
},
onDragOver: (item, index, e) => {
e.stopPropagation();
e.preventDefault();
setDropTarget(null);
},
onClick: (item, index, e) => {
// e.preventDefault();
e.persist();
setSelected((prev) => nextSelected(prev, item, index, e));
},
onMouseDown: (item, index, e) => {
if (selectionIndexByItemIndex(index) === -1) {
e.persist();
setSelected((prev) => nextSelected(prev, item, index, e));
}
},
onDoubleClick: (item, index) => {
// if (context_menu) hideContextMenu();
setSelected([{ item, index }]);
@ -154,25 +145,6 @@ const TrackList = ({
* Also, fix shuffle play; first track goes first, but all subsequent ones go to end of tracklist
*/
const onDragLeave = (e) => {
e.persist();
const { target, relatedTarget } = e;
e.stopPropagation();
// The element that we just left is the element with the listener
// Unfortunately this event triggers for EVERY element that is 'left' during dragging, not just
// the one bound to the event. This check allows us to only act when we leave the bound element.
// if (relatedTarget.id === 'tracklist_container') {
console.debug('onDragLeave', e)
if (target.id === 'tracklist-container') {
console.debug('OUTSIDE, WHEE!');
setDropTarget(null);
}
};
const onDragEnd = (e) => {
console.debug('onDragEnd');
setDropTarget(null);
};
const onRemoveTracks = () => {
if (!removeTracks) {
createNotification({
@ -205,7 +177,7 @@ const TrackList = ({
return items;
}
const nextSelected = (prev, item, index, e, sticky = false) => {
const nextSelected = (prev, item, index, e = {}, sticky = false) => {
const alreadySelected = selectionIndexByItemIndex(index);
if (e.shiftKey) {
@ -234,7 +206,18 @@ const TrackList = ({
}
const is_selected = (index) => selected.find(({ index: i }) => index === i);
const is_dropping = (index) => dropTarget === index;
const is_drag_over = (index) => dropTarget === index;
const getDragItem = (item, index) => {
let selectedForDrag = selected;
if (selectionIndexByItemIndex(index) === -1) {
selectedForDrag = nextSelected(selected, item, index);
// setSelected(selectedForDrag);
}
return {
selected: selectedForDrag,
context,
};
};
return (
<SmartList
@ -246,10 +229,10 @@ const TrackList = ({
play_state,
show_source_icon,
context,
selected_tracks,
getDragItem,
can_sort: context?.can_edit,
is_selected,
// is_dropping,
is_drag_over,
mini_zones: slim_mode || isTouchDevice(),
events,
}}

View File

@ -8,7 +8,7 @@ import Button from '../../components/Button';
import RelatedArtists from '../../components/RelatedArtists';
import { i18n, I18n } from '../../locale';
import { sortItems, applyFilter } from '../../util/arrays';
import { encodeUri } from '../../util/format';
import { encodeUri, formatContext } from '../../util/format';
import { makeSortSelector } from '../../util/selectors';
import {
hideContextMenu,
@ -29,7 +29,6 @@ export default ({
const [sortField, sortReverse] = useSelector(sortSelector);
const {
uri,
name,
tracks,
related_artists,
} = artist;
@ -102,12 +101,7 @@ export default ({
{tracks && <h4><I18n path="artist.overview.top_tracks" /></h4>}
<div className="list-wrapper">
<TrackList
source={{
uri,
name,
type: 'artist',
context: 'artist',
}}
context={formatContext(artist)}
className="artist-track-list"
uri={uri}
tracks={tracks ? tracks.slice(0, 10) : []}

View File

@ -61,7 +61,7 @@
width: 32px;
}
&.drag-over {
&--drag-over {
border-color: colour('blue');
&:before {

View File

@ -288,7 +288,11 @@
}
}
&.drag-over {
&--dragging {
opacity: 0.5;
}
&--drag-over {
&:before {
opacity: 1;
}