React DnD as provider working pretty well, still WIP
This commit is contained in:
130
src/js/App.js
130
src/js/App.js
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
|
||||
@ -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) : []}
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
&.drag-over {
|
||||
&--drag-over {
|
||||
border-color: colour('blue');
|
||||
|
||||
&:before {
|
||||
|
||||
@ -288,7 +288,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.drag-over {
|
||||
&--dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&--drag-over {
|
||||
&:before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user