This commit is contained in:
James Barnsley
2022-09-04 09:03:11 +12:00
parent 5715c2c507
commit 33bb054e86
117 changed files with 8419 additions and 25110 deletions

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Route, Switch, useLocation, useHistory } from 'react-router-dom';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import ReactGA from 'react-ga';
import { DndProvider } from 'react-dnd';
@ -42,12 +42,50 @@ import * as snapcastActions from './services/snapcast/actions';
import MediaSession from './components/MediaSession';
import ErrorBoundary from './components/ErrorBoundary';
const Content = () => (
<>
<Sidebar tabIndex="3" />
<PlaybackControls tabIndex="2"/>
<main id="main" className="smooth-scroll" tabIndex="1">
<Routes>
<Route path="" element={<Queue />} />
<Route path="queue/" element={<Queue />} />
<Route path="queue/history" element={<QueueHistory />}/>
<Route path="settings/debug" element={<Debug />} />
<Route path="settings/*" element={<Settings />} />
<Route path="search/" element={<Search />} />
<Route path="search/:type/:term" element={<Search />} />
<Route path="artist/:uri/*" element={<Artist />} />
<Route path="album/:uri/" element={<Album />} />
<Route path="album/:uri/:name" element={<Album />} />
<Route path="playlist/:uri/" element={<Playlist />} />
<Route path="playlist/:uri/:name" element={<Playlist />} />
<Route path="user/:uri/" element={<User />} />
<Route path="user/:uri/:name" element={<User />} />
<Route path="track/:uri/" element={<Track />} />
<Route path="track/:uri/:name" element={<Track />} />
<Route path="uri/:uri/" element={<UriRedirect />} />
<Route path="uri/:uri/:name" element={<UriRedirect />} />
<Route path="discover/*" element={<Discover />} />
<Route path="library/*" element={<Library />} />
<Route element={
<ErrorMessage type="not-found" title="Not found">
<p>Oops, that link could not be found</p>
</ErrorMessage>
} />
</Routes>
</main>
</>
);
const App = () => {
const [isReady, setIsReady] = useState(false);
const [hasInteracted, setHasInteracted] = useState(false);
const { pathname, state: { scroll_position } = {} } = useLocation();
const location = useLocation();
const { pathname, state } = location;
const scroll_position = state?.scroll_position;
const dispatch = useDispatch();
const history = useHistory();
const navigate = useNavigate();
const language = useSelector((state) => state.ui.language);
window.language = language;
@ -165,7 +203,7 @@ const App = () => {
dispatch(snapcastActions.connect());
}
if (!initial_setup_complete) {
history.push('/modal/initial-setup');
navigate('/modal/initial-setup');
}
}
}, [isReady]);
@ -253,65 +291,10 @@ const App = () => {
>
<div className="body">
<ModalStateListener />
<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} />
<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
path="/discover"
component={Discover}
/>
<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>
<Routes>
<Route path="modal/*" element={<Modals />} />
<Route path="*" element={<Content />} />
</Routes>
</div>
<ResizeListener />

View File

@ -1,25 +1,7 @@
import React, { useEffect, useState, useRef } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { withRouter } from 'react-router';
import { compact } from 'lodash';
import {
uriSource,
uriType,
getFromUri,
buildLink,
isLoading,
throttle,
titleCase,
} 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';
import * as coreActions from '../../services/core/actions';
import * as uiActions from '../../services/ui/actions';
@ -27,8 +9,6 @@ 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,
@ -207,4 +187,4 @@ const mapDispatchToProps = (dispatch) => ({
mopidyActions: bindActionCreators(mopidyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(ContextMenu));
export default connect(mapStateToProps, mapDispatchToProps)(ContextMenu);

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { uniq } from 'lodash';
import { I18n } from '../../locale';
import Link from '../Link';
@ -266,12 +266,12 @@ const Discover = ({
uris,
disabled,
}) => {
const history = useHistory();
const navigate = useNavigate();
const dispatch = useDispatch();
const onClick = () => {
dispatch(hideContextMenu());
const discoverUri = encodeUri(`iris:discover:${uris.map((uri) => encodeUri(uri)).join(',')}`);
history.push(`/discover/recommendations/${discoverUri}`);
navigate(`/discover/recommendations/${discoverUri}`);
};
return (
<div className={`context-menu__item ${disabled && 'context-menu__item--disabled'}`}>
@ -380,11 +380,11 @@ const GoTo = ({
type,
uri,
}) => {
const history = useHistory();
const navigate = useNavigate();
const dispatch = useDispatch();
const onClick = () => {
dispatch(hideContextMenu());
history.push(`/${type}/${encodeUri(uri)}`);
navigate(`/${type}/${encodeUri(uri)}`);
};
return (
<div className="context-menu__item">

View File

@ -1,6 +1,6 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { useDragLayer, useDrop } from 'react-dnd';
import * as mopidyActions from '../../services/mopidy/actions';
import Icon from '../Icon';
@ -53,7 +53,7 @@ const Dropzone = ({
accept,
}) => {
const dispatch = useDispatch();
const history = useHistory();
const navigate = useNavigate();
const [{ handlerId, isOver, canDrop }, drop] = useDrop({
accept,
collect: (monitor) => {
@ -73,10 +73,10 @@ const Dropzone = ({
dispatch(mopidyActions.enqueueURIs({ uris, from: context, play_next: true }));
break;
case 'add_to_playlist':
history.push(`/modal/add-to-playlist/${encodeUri(uris.join(','))}`);
navigate(`/modal/add-to-playlist/${encodeUri(uris.join(','))}`);
break;
case 'create_playlist_and_add':
history.push(`/modal/create-playlist/${encodeUri(uris.join(','))}`);
navigate(`/modal/create-playlist/${encodeUri(uris.join(','))}`);
break;
default:
break;

View File

@ -1,6 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { uriType } from '../../util/helpers';
import Icon from '../Icon';
import { i18n } from '../../locale';
@ -12,7 +11,7 @@ const SearchForm = ({
onReset: doReset,
onSubmit: doSubmit,
}) => {
const history = useHistory();
const navigate = useNavigate();
const [term, setTerm] = useState(termProp);
const [dirty, setDirty] = useState();
@ -43,19 +42,19 @@ const SearchForm = ({
// check for uri type matching
switch (uriType(term)) {
case 'album':
history.push(`/album/${encodeUri(term)}`);
navigate(`/album/${encodeUri(term)}`);
break;
case 'artist':
history.push(`/artist/${encodeUri(term)}`);
navigate(`/artist/${encodeUri(term)}`);
break;
case 'playlist':
history.push(`/playlist/${encodeUri(term)}`);
navigate(`/playlist/${encodeUri(term)}`);
break;
case 'track':
history.push(`/track/${encodeUri(term)}`);
navigate(`/track/${encodeUri(term)}`);
break;
default:

View File

@ -32,6 +32,7 @@ const SecondaryLine = ({
const items = () => {
switch (type) {
case 'mood':
case 'directory':
case 'playlist_group':
return null;
case 'playlist':
@ -78,7 +79,7 @@ const GridItem = ({
type: item?.type?.toUpperCase() || 'UNKNOWN',
item: { item, context: item },
});
const tile = ['playlist_group', 'mood', 'directory', 'category'].indexOf(item?.type) > -1
const tile = ['playlist_group', 'mood', 'category'].indexOf(item?.type) > -1
const onContextMenu = (e) => {
e.preventDefault();
@ -142,7 +143,10 @@ const GridItem = ({
type={item.type}
/>
<div className="grid__item__name">
{item.name ? item.name : <span className="opaque-text">{item.uri}</span>}
{item.name
? <span title={item.name}>{item.name}</span>
: <span className="opaque-text">{item.uri}</span>
}
</div>
<div className="grid__item__secondary">
<SecondaryLine item={item} sourceIcon={sourceIcon} />

View File

@ -1,6 +1,6 @@
import ReactGA from 'react-ga';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { useHotkeys } from 'react-hotkeys-hook';
import { indexToArray, sortItems } from '../util/arrays';
import { collate } from '../util/format';
@ -10,7 +10,7 @@ import * as mopidyActions from '../services/mopidy/actions';
import * as snapcastActions from '../services/snapcast/actions';
const Hotkeys = () => {
const history = useHistory();
const navigate = useNavigate();
const dispatch = useDispatch();
const volume = useSelector((state) => state.mopidy.volume);
const mute = useSelector((state) => state.mopidy.mute);
@ -94,7 +94,7 @@ const Hotkeys = () => {
e,
label: 'Hotkey info',
callback: () => {
history.push('/modal/hotkeys');
navigate('/modal/hotkeys');
},
});
});

View File

@ -1,11 +1,11 @@
import React from 'react';
import { Link as RouterLink, useHistory, useLocation } from 'react-router-dom';
import { Link as RouterLink, useNavigate, useLocation } from 'react-router-dom';
import { scrollTo } from '../util/helpers';
const updateScrollPosition = ({
retainScroll = false,
scrollTarget = 0,
history,
navigate,
location: {
state,
pathname,
@ -16,14 +16,17 @@ const updateScrollPosition = ({
// This doesn't trigger lazy-load elements (unless scrolling exposes the LazyLoader component).
const main = document.getElementById('main');
history.replace(
navigate(
pathname,
{
...state,
scroll_position: main.scrollTop,
previous: {
pathname,
state: {
...state,
scroll_position: main.scrollTop,
previous: {
pathname,
},
},
replace: true,
},
);
@ -48,23 +51,23 @@ const Link = ({
children,
}) => {
if (!to) return <span className={className}>{children}</span>;
const history = useHistory();
const navigate = useNavigate();
const location = useLocation();
const handleClick = (e) => {
updateScrollPosition({
history, location, retainScroll, scrollTarget,
navigate, location, retainScroll, scrollTarget,
});
if (onClick) onClick(e);
}
// Decode both links. This handles issues where one link is encoded and the other isn't, but
// they're otherwise identical
const currentLink = history?.location?.pathname || '';
const currentLink = location?.pathname || '';
const isLinkActive = exact ? currentLink === to : currentLink.startsWith(to);
// We have an active detector method
// This is used almost solely by the Sidebar navigation
const active = history && isLinkActive ? (activeClassName || 'active') : '';
const active = navigate && isLinkActive ? (activeClassName || 'active') : '';
return (
<RouterLink
onClick={handleClick}

View File

@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { useNavigate, useLocation } from 'react-router-dom';
import { useDrag } from 'react-dnd';
import LinksSentence from './LinksSentence';
import { dater } from './Dater';
@ -98,7 +98,7 @@ const ListItem = ({
const dispatch = useDispatch();
const spotify_available = useSelector((state) => state.spotify.access_token);
const history = useHistory();
const navigate = useNavigate();
const location = useLocation();
const [_, drag] = useDrag({
type: item?.type?.toUpperCase() || 'UNKNOWN',
@ -154,9 +154,9 @@ const ListItem = ({
}
if (e.target.tagName.toLowerCase() !== 'a') {
updateScrollPosition({ location, history });
updateScrollPosition({ location, navigate });
e.preventDefault();
history.push(to);
navigate(to);
}
};

View File

@ -1,13 +1,13 @@
import { useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { useNavigate, useLocation } from 'react-router-dom';
const ModalStateListener = () => {
const { name } = useSelector((state) => state.ui.modal || {});
const history = useHistory();
const navigate = useNavigate();
const location = useLocation();
if (name && location.pathname.indexOf(`modal/${name}`) < 0) {
history.push(`modal/${name}`);
navigate(`modal/${name}`);
}
return null;

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Route, Switch, useParams, useHistory } from 'react-router-dom';
import { Route, Routes, useParams, useNavigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import Link from './Link';
import Icon from './Icon';
@ -116,7 +116,7 @@ const Server = () => {
};
const Menu = () => {
const history = useHistory();
const navigate = useNavigate();
const dispatch = useDispatch();
const servers = indexToArray(useSelector((state) => state.mopidy.servers));
@ -133,7 +133,7 @@ const Menu = () => {
const addServer = () => {
const action = mopidyActions.addServer();
dispatch(action);
history.push(`/settings/servers/${action.server.id}`);
navigate(`/settings/servers/${action.server.id}`);
};
return (
@ -168,7 +168,6 @@ const Menu = () => {
}
return (
<Link
history={history}
className="menu-item"
activeClassName="menu-item--active"
to={`/settings/servers/${server.id}`}
@ -207,13 +206,12 @@ const Menu = () => {
const Servers = () => (
<div className="sub-tabs sub-tabs--servers">
<Menu />
<Switch>
<Routes>
<Route
exact
path="/settings/servers/:id"
component={Server}
path=":id"
component={<Server />}
/>
</Switch>
</Routes>
</div>
);

View File

@ -18,6 +18,7 @@ import * as mopidyActions from '../services/mopidy/actions';
import * as pusherActions from '../services/pusher/actions';
import * as spotifyActions from '../services/spotify/actions';
import * as lastfmActions from '../services/lastfm/actions';
import { withRouter } from '../util';
import * as geniusActions from '../services/genius/actions';
import { I18n, i18n } from '../locale';
import Button from './Button';
@ -291,7 +292,6 @@ class Services extends React.Component {
me: geniusUser,
authorization: geniusAuthorization,
},
history,
snapcast: {
enabled: snapcastEnabled,
connected: snapcastConnected,
@ -335,7 +335,6 @@ class Services extends React.Component {
<div className="sub-tabs__menu menu" id="services-menu">
<div className="menu__inner">
<Link
history={history}
className="menu-item menu-item--snapcast"
activeClassName="menu-item--active"
to="/settings/services/snapcast"
@ -364,7 +363,6 @@ class Services extends React.Component {
</div>
</Link>
<Link
history={history}
className="menu-item menu-item--spotify"
activeClassName="menu-item--active"
to="/settings/services/spotify"
@ -387,7 +385,6 @@ class Services extends React.Component {
</div>
</Link>
<Link
history={history}
className="menu-item menu-item--lastfm"
activeClassName="menu-item--active"
to="/settings/services/lastfm"
@ -410,7 +407,6 @@ class Services extends React.Component {
</div>
</Link>
<Link
history={history}
className="menu-item menu-item--genius"
activeClassName="menu-item--active"
to="/settings/services/genius"
@ -438,8 +434,8 @@ class Services extends React.Component {
}
renderService() {
const { match } = this.props;
switch (match.params.service) {
const { params } = this.props;
switch (params.service) {
case 'spotify':
return <div className="sub-tabs__content">{this.renderSpotify()}</div>;
case 'lastfm':
@ -447,7 +443,7 @@ class Services extends React.Component {
case 'genius':
return <div className="sub-tabs__content">{this.renderGenius()}</div>;
case 'snapcast':
return <div className="sub-tabs__content"><Snapcast match={this.props.match} /></div>;
return <div className="sub-tabs__content"><Snapcast params={params} /></div>;
default:
return null;
}
@ -475,4 +471,4 @@ const mapDispatchToProps = (dispatch) => ({
geniusActions: bindActionCreators(geniusActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Services);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Services));

View File

@ -6,15 +6,12 @@ import SnapcastGroups from './SnapcastGroups';
import * as uiActions from '../services/ui/actions';
import * as actions from '../services/snapcast/actions';
import { I18n } from '../locale';
import Button from './Button';
const Snapcast = (props) => {
const {
actions,
show_disconnected_clients,
uiActions,
match,
history,
snapcast: {
host,
port,
@ -125,7 +122,7 @@ const Snapcast = (props) => {
</div>
</div>
{connected && enabled && <SnapcastGroups match={match} history={history} />}
{connected && enabled && <SnapcastGroups />}
</div>
);
};

View File

@ -1,4 +1,5 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { collate } from '../util/format';
@ -21,10 +22,9 @@ const SnapcastGroups = (props) => {
streams,
groups,
clients,
history,
match: { params: { id: groupId } },
} = props;
const { id: groupId } = useParams();
const streamsArray = Object.keys(streams).map((id) => streams[id]);
const groupsArray = Object.keys(groups).map((id) => groups[id]);
@ -128,7 +128,6 @@ const SnapcastGroups = (props) => {
className={`snapcast__groups__menu-item menu-item${noClients ? ' menu-item--no-clients' : ''}`}
activeClassName="menu-item--active"
key={group.id}
history={history}
to={`/settings/services/snapcast/${group.id}`}
scrollTo="#services-snapcast-groups"
>

View File

@ -1,21 +1,24 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from './store/index';
import App from './App';
import { _ } from 'core-js';
require('../scss/app.scss');
ReactDOM.render(
const root = createRoot(document.getElementById('app'));
root.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<BrowserRouter basename="/iris">
<Route path="/" component={App} />
<Routes>
<Route path="*" element={<App />} />
</Routes>
</BrowserRouter>
</PersistGate>
</Provider>,
document.getElementById('app'),
);

View File

@ -15,9 +15,7 @@ import {
formatAlbum,
formatTrack,
formatTracks,
formatPlaylists,
formatPlaylistGroup,
formatPlaylistGroups,
getTrackIcon,
formatArtists,
formatArtist,
@ -412,7 +410,17 @@ const MopidyMiddleware = (function () {
break;
}
socket.on((type, data) => handleMessage(socket, store, type, data));
socket.on('state:online', (data) => handleMessage(socket, store, 'state:online', data));
socket.on('state:offline', (data) => handleMessage(socket, store, 'state:offline', data));
socket.on('event:tracklistChanged', (data) => handleMessage(socket, store, 'event:tracklistChanged', data));
socket.on('event:playbackStateChanged', (data) => handleMessage(socket, store, 'event:playbackStateChanged', data));
socket.on('event:seeked', (data) => handleMessage(socket, store, 'event:seeked', data));
socket.on('event:trackPlaybackEnded', (data) => handleMessage(socket, store, 'event:trackPlaybackEnded', data));
socket.on('event:trackPlaybackStarted', (data) => handleMessage(socket, store, 'event:trackPlaybackStarted', data));
socket.on('event:volumeChanged', (data) => handleMessage(socket, store, 'event:volumeChanged', data));
socket.on('event:muteChanged', (data) => handleMessage(socket, store, 'event:muteChanged', data));
socket.on('event:optionsChanged', (data) => handleMessage(socket, store, 'event:optionsChanged', data));
socket.on('event:streamTitleChanged', (data) => handleMessage(socket, store, 'event:streamTitleChanged', data));
break;
}
@ -1803,6 +1811,7 @@ const MopidyMiddleware = (function () {
const subdirectories = [];
const trackUrisToLoad = [];
const subdirectoryImagesToLoad = [];
const playlistsToLoad = [];
results.forEach((item) => {
if (item.__model__ === 'Track') {
@ -1829,8 +1838,10 @@ const MopidyMiddleware = (function () {
} else {
subdirectories.push(formatPlaylist({ ...item, loading: true }));
subdirectoryImagesToLoad.push(item.uri);
playlistsToLoad.push(item.uri);
}
} else {
subdirectoryImagesToLoad.push(item.uri);
subdirectories.push(item);
}
});
@ -1862,30 +1873,75 @@ const MopidyMiddleware = (function () {
});
}
if (subdirectoryImagesToLoad.length) {
console.info(`Loading ${subdirectoryImagesToLoad.length} subdirectory URIs`);
request(store, 'library.getImages', { uris: subdirectoryImagesToLoad })
.then((response) => {
const subdirectoriesWithImages = subdirectories.map((subdir) => {
let images = response[subdir.uri] || undefined;
if (images) {
images = formatImages(digestMopidyImages(store.getState().mopidy, images));
}
return {
...subdir,
images,
};
const imagesLoaded = new Promise((resolve) => {
if (subdirectoryImagesToLoad.length) {
console.info(`Loading ${subdirectoryImagesToLoad.length} subdirectory URIs`);
request(store, 'library.getImages', { uris: subdirectoryImagesToLoad })
.then((response) => {
resolve(response);
});
} else {
resolve({});
}
});
store.dispatch({
type: 'MOPIDY_DIRECTORY_LOADED',
directory: {
uri,
subdirectories: subdirectoriesWithImages,
},
});
const playlistsLoaded = new Promise((resolve) => {
const playlists = {};
if (playlistsToLoad.length) {
console.info(`Loading ${playlistsToLoad.length} playlist URIs`);
const toLoad = playlistsToLoad.length;
let loaded = 0;
playlistsToLoad.map((uri) => {
request(store, 'playlists.lookup', { uri })
.then((playlist) => {
playlists[uri] = formatPlaylist({
name: playlist.name,
uri: playlist.uri,
tracks: formatTracks(playlist.tracks),
});
loaded += 1;
if (loaded === toLoad) {
resolve(playlists);
}
})
});
}
} else {
resolve(playlists);
}
});
imagesLoaded.then((response) => {
const subdirectoriesWithImages = subdirectories.map((subdir) => {
let images = response[subdir.uri] || undefined;
if (images) {
images = formatImages(digestMopidyImages(store.getState().mopidy, images));
}
return {
...subdir,
images,
};
});
playlistsLoaded.then((playlists) => {
const subdirectoriesWithPlaylists = subdirectoriesWithImages.map((subdir) => {
const playlist = playlists[subdir.uri] || {};
return {
...subdir,
...playlist,
};
});
store.dispatch({
type: 'MOPIDY_DIRECTORY_LOADED',
directory: {
uri,
subdirectories: subdirectoriesWithPlaylists,
},
});
});
});
};
const getBrowse = () => request(store, 'library.browse', { uri })

View File

@ -538,8 +538,8 @@ const formatPlaylist = function (data) {
playlist.followers = data.followers.total;
}
if (data.tracks && data.tracks.total !== undefined) {
playlist.tracks_total = data.tracks.total;
if (data.tracks && data.tracks_total !== undefined) {
playlist.tracks_total = data.tracks_total;
}
if (playlist.last_modified && playlist.added_at === undefined) {

View File

@ -4,6 +4,7 @@ import * as selectors from './selectors';
import * as storage from './storage';
import * as format from './format';
import * as debug from './debug';
import withRouter from './withRouter';
export {
debug,
@ -12,6 +13,7 @@ export {
selectors,
storage,
format,
withRouter,
};
export default {
@ -21,4 +23,5 @@ export default {
selectors,
storage,
format,
withRouter,
};

18
src/js/util/withRouter.js Normal file
View File

@ -0,0 +1,18 @@
import React from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
const withRouter = (WrappedComponent) => (props) => {
const params = useParams();
const navigate = useNavigate();
const location = useLocation();
return (
<WrappedComponent
{...props}
params={params}
navigate={navigate}
location={location}
/>
);
};
export default withRouter;

View File

@ -23,7 +23,7 @@ import { uriSource } from '../util/helpers';
import Button from '../components/Button';
import { makeItemSelector, makeSortSelector } from '../util/selectors';
import { applyFilter, sortItems } from '../util/arrays';
import { decodeUri, formatSimpleObject, formatContext } from '../util/format';
import { decodeUri, formatContext } from '../util/format';
const SORT_KEY = 'album_tracks';

View File

@ -6,7 +6,7 @@ import {
uriSource,
titleCase,
} from '../../util/helpers';
import { i18n, I18n } from '../../locale';
import { I18n } from '../../locale';
import { nice_number } from '../../components/NiceNumber';
export default ({

View File

@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react';
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Route, Switch, useParams, useHistory } from 'react-router-dom';
import { Route, Routes, useParams, useNavigate } from 'react-router-dom';
import ErrorMessage from '../../components/ErrorMessage';
import Link from '../../components/Link';
import Thumbnail from '../../components/Thumbnail';
@ -26,7 +25,6 @@ import Button from '../../components/Button';
import { makeItemSelector } from '../../util/selectors';
const Artist = () => {
const history = useHistory();
const dispatch = useDispatch();
const { uri: encodedUri } = useParams();
const uri = decodeUri(encodedUri);
@ -143,7 +141,6 @@ const Artist = () => {
<div className="sub-views" id="sub-views-menu">
<Link
exact
history={history}
activeClassName="sub-views__option--active"
className="sub-views__option"
to={`/artist/${encodeUri(uri)}`}
@ -154,7 +151,6 @@ const Artist = () => {
{artist.tracks && artist.tracks.length > 10 && (
<Link
exact
history={history}
activeClassName="sub-views__option--active"
className="sub-views__option"
to={`/artist/${encodeUri(uri)}/tracks`}
@ -166,7 +162,6 @@ const Artist = () => {
{artist.related_artists && (
<Link
exact
history={history}
activeClassName="sub-views__option--active"
className="sub-views__option"
to={`/artist/${encodeUri(uri)}/related-artists`}
@ -177,7 +172,6 @@ const Artist = () => {
)}
<Link
exact
history={history}
activeClassName="sub-views__option--active"
className="sub-views__option"
to={`/artist/${encodeUri(uri)}/about`}
@ -189,20 +183,20 @@ const Artist = () => {
</div>
</div>
<div className="content-wrapper">
<Switch>
<Route exact path="/artist/:id/related-artists">
<Routes>
<Route path="related-artists" element={
<Related artist={artist} />
</Route>
<Route exact path="/artist/:id/tracks">
}/>
<Route path="tracks" element={
<Tracks artist={artist} />
</Route>
<Route exact path="/artist/:id/about">
}/>
<Route path="about" element={
<About artist={artist} />
</Route>
<Route exact path="/artist/:id/:name?">
}/>
<Route path="" element={
<Overview artist={artist} albums={albums} />
</Route>
</Switch>
}/>
</Routes>
</div>
</div>
);

View File

@ -1,5 +1,4 @@
import React, { useState } from 'react';
import { Grid } from '../../components/Grid';
import { useDispatch, useSelector } from 'react-redux';
import TrackList from '../../components/TrackList';
import FilterField from '../../components/Fields/FilterField';
@ -16,11 +15,7 @@ import { formatContext } from '../../util/format';
const SORT_KEY = 'artist_tracks';
export default ({ artist }) => {
const {
uri,
name,
tracks: tracksProp,
} = artist || {};
const { tracks: tracksProp } = artist || {};
const dispatch = useDispatch();
const [search, setSearch] = useState('');
const [sortField, sortReverse] = useSelector(

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { Route, Routes } from 'react-router-dom';
import Recommendations from './Recommendations';
import FeaturedPlaylists from './FeaturedPlaylists';
import NewReleases from './NewReleases';
@ -8,41 +8,50 @@ import PlaylistGroup from './PlaylistGroup';
import Playlist from '../Playlist';
export default () => (
<Switch>
<Routes>
<Route
exact
path="/discover/recommendations/:uri?"
component={Recommendations}
path="recommendations/"
element={<Recommendations />}
/>
<Route
exact
path="/discover/moods"
component={Moods}
path="recommendations/:uri"
element={<Recommendations />}
/>
<Route
exact
path="/discover/moods/:uri/:name?"
component={PlaylistGroup}
path="moods"
element={<Moods />}
/>
<Route
exact
path="/discover/featured-playlists"
component={FeaturedPlaylists}
path="moods/:uri/"
element={<PlaylistGroup />}
/>
<Route
exact
path="/discover/featured-playlists/playlist/:uri/:name?"
component={Playlist}
path="moods/:uri/:name"
element={<PlaylistGroup />}
/>
<Route
exact
path="/discover/featured-playlists/playlist_group/:uri/:name?"
component={PlaylistGroup}
path="featured-playlists"
element={<FeaturedPlaylists />}
/>
<Route
exact
path="/discover/new-releases"
component={NewReleases}
path="featured-playlists/playlist/:uri/"
element={<Playlist />}
/>
</Switch>
<Route
path="featured-playlists/playlist/:uri/:name/"
element={<Playlist />}
/>
<Route
path="featured-playlists/playlist_group/:uri/"
element={<PlaylistGroup />}
/>
<Route
path="featured-playlists/playlist_group/:uri/:name"
element={<PlaylistGroup />}
/>
<Route
path="new-releases/"
element={<NewReleases />}
/>
</Routes>
);

View File

@ -16,7 +16,7 @@ import {
makeItemSelector,
makeLoadingSelector,
} from '../../util/selectors';
import { formatSimpleObject, formatContext } from '../../util/format';
import { formatContext } from '../../util/format';
class NewReleases extends React.Component {
componentDidMount() {

View File

@ -86,11 +86,11 @@ class Browse extends React.Component {
case 'SomaFM':
subdirectory.icons = ['/iris/assets/backgrounds/browse-somafm.jpg'];
break;
case 'Jellyfin':
case 'Jellyfin':
subdirectory.icons = ['/iris/assets/backgrounds/browse-jellyfin.jpg'];
break;
case 'Podcasts':
subdirectory.icons = ['/iris/assets/backgrounds/browse-podcasts.jpg'];
break;
@ -109,8 +109,17 @@ class Browse extends React.Component {
subdirectory.icons = ['/iris/assets/backgrounds/browse-youtube.jpg'];
break;
default:
case 'bandcamp':
case 'Bandcamp':
subdirectory.icons = ['/iris/assets/backgrounds/browse-default.jpg'];
break;
default:
if (subdirectory.images?.medium) {
subdirectory.icons = [subdirectory.images?.medium];
} else {
subdirectory.icons = ['/iris/assets/backgrounds/browse-default.jpg'];
}
}
grid_items.push({

View File

@ -10,7 +10,6 @@ import DropdownField from '../../components/Fields/DropdownField';
import FilterField from '../../components/Fields/FilterField';
import Icon from '../../components/Icon';
import ErrorBoundary from '../../components/ErrorBoundary';
import LazyLoadListener from '../../components/LazyLoadListener';
import * as uiActions from '../../services/ui/actions';
import * as mopidyActions from '../../services/mopidy/actions';
import * as spotifyActions from '../../services/spotify/actions';
@ -20,6 +19,7 @@ import Button from '../../components/Button';
import { encodeUri, decodeUri } from '../../util/format';
import { makeLoadingSelector } from '../../util/selectors';
import ErrorMessage from '../../components/ErrorMessage';
import { withRouter } from '../../util';
const Breadcrumbs = ({ uri }) => {
let parent_uri = uri || null;
@ -258,7 +258,7 @@ const mapStateToProps = (state, ownProps) => {
library_directory_view: view,
},
} = state;
const uri = decodeUri(ownProps.match.params.uri);
const uri = decodeUri(ownProps.params.uri);
const uriMatcher = [uri, decodeURIComponent(uri)]; // Lenient matching due to encoding diffs
const directory = _directory && uriMatcher.includes(_directory.uri)
? _directory
@ -266,7 +266,7 @@ const mapStateToProps = (state, ownProps) => {
return {
uri,
name: decodeURIComponent(ownProps.match.params.name),
name: decodeURIComponent(ownProps.params.name),
loading: loadingSelector(state),
directory,
view,
@ -279,4 +279,4 @@ const mapDispatchToProps = (dispatch) => ({
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(BrowseDirectory);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(BrowseDirectory));

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { Route, Routes } from 'react-router-dom';
import Artists from './Artists';
import Albums from './Albums';
import Tracks from './Tracks';
@ -8,36 +8,30 @@ import Browse from './Browse';
import BrowseDirectory from './BrowseDirectory';
export default () => (
<Switch>
<Routes>
<Route
exact
path="/library/artists"
component={Artists}
path="artists/"
element={<Artists />}
/>
<Route
exact
path="/library/albums"
component={Albums}
path="albums/"
element={<Albums />}
/>
<Route
exact
path="/library/tracks"
component={Tracks}
path="tracks/"
element={<Tracks />}
/>
<Route
exact
path="/library/playlists"
component={Playlists}
path="playlists/"
element={<Playlists />}
/>
<Route
exact
path="/library/browse"
component={Browse}
path="browse/"
element={<Browse />}
/>
<Route
exact
path="/library/browse/:name/:uri"
component={BrowseDirectory}
path="browse/:name/:uri"
element={<BrowseDirectory />}
/>
</Switch>
</Routes>
);

View File

@ -18,6 +18,7 @@ import {
makeProvidersSelector,
makeLibrarySelector,
} from '../../util/selectors';
import { withRouter } from '../../util';
const processKeys = [
'MOPIDY_GET_LIBRARY_PLAYLISTS',
@ -136,7 +137,7 @@ const mapStateToProps = (state, ownProps) => {
},
} = state;
const unencodedUris = ownProps.match.params?.uris;
const unencodedUris = ownProps.params?.uris;
const uris = unencodedUris ? decodeUri(unencodedUris).split(',') : [];
return {
@ -156,4 +157,4 @@ const mapDispatchToProps = (dispatch) => ({
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(AddToPlaylist);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(AddToPlaylist));

View File

@ -10,6 +10,7 @@ import * as uiActions from '../../services/ui/actions';
import { scrollTo, generateGuid } from '../../util/helpers';
import { i18n, I18n } from '../../locale';
import Button from '../../components/Button';
import { withRouter } from '../../util';
class EditCommand extends React.Component {
constructor(props) {
@ -264,7 +265,7 @@ class EditCommand extends React.Component {
}
const mapStateToProps = (state, ownProps) => {
const { id } = ownProps.match.params;
const { id } = ownProps.params;
return {
id,
command: (id && state.pusher.commands && state.pusher.commands[id] !== undefined ? state.pusher.commands[id] : null),
@ -276,4 +277,4 @@ const mapDispatchToProps = (dispatch) => ({
uiActions: bindActionCreators(uiActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(EditCommand);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(EditCommand));

View File

@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Modal from './Modal';
import * as uiActions from '../../services/ui/actions';
import { withRouter } from '../../util';
class ImageZoom extends React.Component {
componentDidMount() {
@ -24,4 +25,4 @@ const mapDispatchToProps = (dispatch) => ({
uiActions: bindActionCreators(uiActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(ImageZoom);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ImageZoom));

View File

@ -8,6 +8,7 @@ import * as pusherActions from '../../services/pusher/actions';
import * as mopidyActions from '../../services/mopidy/actions';
import { queryString, isHosted } from '../../util/helpers';
import { I18n, i18n } from '../../locale';
import { withRouter } from '../../util';
import Button from '../../components/Button';
class InitialSetup extends React.Component {
@ -69,14 +70,11 @@ class InitialSetup extends React.Component {
// Wait a jiffy to allow changes to apply to store
setTimeout(() => {
self.props.uiActions.closeModal();
// We've changed a connection setting, so need to reload
if (self.state.host !== self.props.host || self.state.port !== self.props.port) {
window.location = '/';
// Safe to just close modal
} else {
self.props.history.push('/');
self.props.navigate('/');
}
},
200);
@ -196,4 +194,4 @@ const mapDispatchToProps = (dispatch) => ({
mopidyActions: bindActionCreators(mopidyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(InitialSetup);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(InitialSetup));

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { Route, Routes } from 'react-router-dom';
import EditPlaylist from './EditPlaylist';
import CreatePlaylist from './CreatePlaylist';
import EditRadio from './EditRadio';
@ -16,20 +16,23 @@ import Reset from './Reset';
import Servers from './Servers';
export default () => (
<Switch>
<Route path="/modal/initial-setup" component={InitialSetup} />
<Route path="/modal/kiosk-mode" component={KioskMode} />
<Route path="/modal/add-to-playlist/:uris" component={AddToPlaylist} />
<Route path="/modal/image-zoom" component={ImageZoom} />
<Route path="/modal/hotkeys" component={HotkeysInfo} />
<Route path="/modal/share-config" component={ShareConfig} />
<Route path="/modal/import-config/:source?" component={ImportConfig} />
<Route path="/modal/reset" component={Reset} />
<Route path="/modal/servers" component={Servers} />
<Route path="/modal/edit-command/:id?" component={EditCommand} />
<Route path="/modal/radio" component={EditRadio} />
<Route path="/modal/add-uri" component={AddToQueue} />
<Route path="/modal/create-playlist/:uris?" component={CreatePlaylist} />
<Route path="/modal/edit-playlist/:uri" component={EditPlaylist} />
</Switch>
<Routes>
<Route path="initial-setup" element={<InitialSetup />} />
<Route path="kiosk-mode" element={<KioskMode />} />
<Route path="add-to-playlist/:uris" element={<AddToPlaylist />} />
<Route path="image-zoom" element={<ImageZoom />} />
<Route path="hotkeys" element={<HotkeysInfo />} />
<Route path="share-config" element={<ShareConfig />} />
<Route path="import-config/" element={<ImportConfig />} />
<Route path="import-config/:source" element={<ImportConfig />} />
<Route path="reset" element={<Reset />} />
<Route path="servers" element={<Servers />} />
<Route path="edit-command/" element={<EditCommand />} />
<Route path="edit-command/:id" element={<EditCommand />} />
<Route path="radio" element={<EditRadio />} />
<Route path="add-uri" element={<AddToQueue />} />
<Route path="create-playlist/" element={<CreatePlaylist />} />
<Route path="create-playlist/:uris" element={<CreatePlaylist />} />
<Route path="edit-playlist/:uri" element={<EditPlaylist />} />
</Routes>
);

View File

@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import Thumbnail from '../../components/Thumbnail';
import LinksSentence from '../../components/LinksSentence';
@ -10,7 +10,7 @@ import { i18n, I18n } from '../../locale';
import { indexToArray } from '../../util/arrays';
const Servers = () => {
const history = useHistory();
const navigate = useNavigate();
const dispatch = useDispatch();
const servers = indexToArray(useSelector((state) => state.mopidy.servers || {}));
const current_server = useSelector((state) => state.mopidy.current_server);
@ -24,7 +24,7 @@ const Servers = () => {
const onClick = (server) => {
dispatch(mopidyActions.setCurrentServer(server));
history.push('/queue');
navigate('/queue');
}
return (

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useParams, useNavigate } from 'react-router-dom';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators } from 'redux';
import ErrorMessage from '../components/ErrorMessage';
@ -136,7 +136,7 @@ const Playlist = ({
playPlaylist,
},
}) => {
const history = useHistory();
const navigate = useNavigate();
const { uri: encodedUri, name } = useParams();
const uri = decodeUri(encodedUri);
const [filter, setFilter] = useState('');
@ -156,7 +156,7 @@ const Playlist = ({
useEffect(() => {
if (playlist && playlist.moved_to) {
history.push(`/playlist/${encodeUri(playlist.moved_to)}/${encodeURIComponent(playlist.name.replace('%', ''))}`);
navigate(`/playlist/${encodeUri(playlist.moved_to)}/${encodeURIComponent(playlist.name.replace('%', ''))}`);
}
}, [playlist]);

View File

@ -1,7 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Switch, Route } from 'react-router-dom';
import Header from '../components/Header';
import Icon from '../components/Icon';
import DropdownField from '../components/Fields/DropdownField';
@ -12,16 +11,13 @@ import * as uiActions from '../services/ui/actions';
import * as mopidyActions from '../services/mopidy/actions';
import * as spotifyActions from '../services/spotify/actions';
import { titleCase } from '../util/helpers';
import { withRouter } from '../util';
import { i18n } from '../locale';
class Search extends React.Component {
constructor(props) {
super(props);
this.state = {
type: props.type || 'all',
term: props.term || '',
};
this.state = { term: props.term || '' };
}
componentDidMount = () => {
@ -38,37 +34,28 @@ class Search extends React.Component {
this.digestUri();
}
componentDidUpdate = ({
type: prevType,
term: prevTerm,
}) => {
const {
type: typeProp,
term: termProp,
} = this.props;
const { type, term } = this.state;
if (prevType !== typeProp || prevTerm !== termProp) {
this.search(type, term);
componentDidUpdate = ({ term: prevTerm }) => {
const { term: termProp } = this.props;
if (prevTerm !== termProp) {
this.search();
}
}
onSubmit = (term) => {
const { type } = this.state;
const { history } = this.props;
const { navigate, type } = this.props;
const encodedTerm = encodeURIComponent(term);
this.setState(
{ term },
() => {
history.push(`/search/${type}/${encodedTerm}`);
navigate(`/search/${type}/${encodedTerm}`);
},
);
}
onReset = () => {
const { history } = this.props;
history.push('/search');
const { navigate } = this.props;
navigate('/search');
}
onSortChange = (value) => {
@ -93,16 +80,10 @@ class Search extends React.Component {
};
digestUri = () => {
const {
type,
term,
} = this.props;
if (type && term) {
this.setState({ type, term }, () => {
this.search();
});
} else if (!term || term === '') {
const { term } = this.props;
if (term) {
this.setState({ term }, this.search);
} else {
this.clearSearch();
}
}
@ -130,11 +111,9 @@ class Search extends React.Component {
type: existingType,
term: existingTerm,
},
} = this.props;
const {
type,
term,
} = this.state;
} = this.props;
const { term } = this.state;
setWindowTitle(i18n('search.title_window', { term: decodeURIComponent(term) }));
@ -163,17 +142,14 @@ class Search extends React.Component {
}
render = () => {
const {
term,
type,
} = this.state;
const { term } = this.state;
const {
uri_schemes,
sort,
sort_reverse,
history,
uri_schemes_search_enabled,
uiActions,
type,
} = this.props;
const sort_options = [
@ -211,6 +187,46 @@ class Search extends React.Component {
</>
);
let searchResults;
switch (type) {
case 'artists':
searchResults = <SearchResults type="artists" query={{ term, type: 'artists' }} />;
break;
case 'albums':
searchResults = <SearchResults type="albums" query={{ term, type: 'albums' }} />;
break;
case 'playlists':
searchResults = <SearchResults type="playlists" query={{ term, type: 'playlists' }} />;
break;
case 'tracks':
searchResults = <SearchResults type="tracks" query={{ term, type: 'tracks' }} />
break;
default:
searchResults = (
<>
<div className="search-result-sections cf">
<section className="search-result-sections__item">
<div className="inner">
<SearchResults type="artists" query={{ term, type: 'artists' }} all />
</div>
</section>
<section className="search-result-sections__item">
<div className="inner">
<SearchResults type="albums" query={{ term, type: 'albums' }} all />
</div>
</section>
<section className="search-result-sections__item">
<div className="inner">
<SearchResults type="playlists" query={{ term, type: 'playlists' }} all />
</div>
</section>
</div>
<SearchResults type="tracks" query={{ term, type: 'tracks' }} all />
</>
);
}
return (
<div className="view search-view">
<Header options={options} uiActions={uiActions}>
@ -219,53 +235,13 @@ class Search extends React.Component {
<SearchForm
key={`search_form_${type}_${term}`}
history={history}
term={term}
onSubmit={this.onSubmit}
onReset={this.onReset}
/>
<div className="content-wrapper">
<Switch>
<Route path="/search/artists/:term">
<SearchResults type="artists" query={{ term, type }} />
</Route>
<Route path="/search/albums/:term">
<SearchResults type="albums" query={{ term, type }} />
</Route>
<Route path="/search/playlists/:term">
<SearchResults type="playlists" query={{ term, type }} />
</Route>
<Route path="/search/tracks/:term">
<SearchResults type="tracks" query={{ term, type }} />
</Route>
<Route path="/search">
<div className="search-result-sections cf">
<section className="search-result-sections__item">
<div className="inner">
<SearchResults type="artists" query={{ term, type }} all />
</div>
</section>
<section className="search-result-sections__item">
<div className="inner">
<SearchResults type="albums" query={{ term, type }} all />
</div>
</section>
<section className="search-result-sections__item">
<div className="inner">
<SearchResults type="playlists" query={{ term, type }} all />
</div>
</section>
</div>
<SearchResults type="tracks" query={{ term, type }} all />
</Route>
</Switch>
{searchResults}
</div>
</div>
);
@ -274,13 +250,13 @@ class Search extends React.Component {
const mapStateToProps = (state, ownProps) => {
const {
match: {
params: {
type,
term,
},
params: {
type,
term,
},
navigation,
} = ownProps;
const {
mopidy: {
uri_schemes = [],
@ -298,8 +274,9 @@ const mapStateToProps = (state, ownProps) => {
} = state;
return {
type,
type: type || 'all',
term,
navigation,
uri_schemes,
uri_schemes_search_enabled,
sort,
@ -315,4 +292,4 @@ const mapDispatchToProps = (dispatch) => ({
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Search);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Search));

View File

@ -1,9 +1,7 @@
import React from 'react';
import { connect, useDispatch } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Route, Link } from 'react-router-dom';
import localForage from 'localforage';
import ConfirmationButton from '../components/Fields/ConfirmationButton';
import { Routes, Route } from 'react-router-dom';
import PusherConnectionList from '../components/PusherConnectionList';
import SourcesPriority from '../components/Fields/SourcesPriority';
import Commands from '../components/Fields/Commands';
@ -205,14 +203,19 @@ class Settings extends React.Component {
</div>
</label>
<Route path="/settings/:server?/:id?" component={Servers} />
<Routes>
<Route path=":server/*" element={<Servers />} />
</Routes>
<h4 className="underline">
<I18n path="settings.services.title" />
<a name="services" />
</h4>
<Route path="/settings/:services?/:service?/:id?" component={Services} />
<Routes>
<Route path="" element={<Services />} />
<Route path=":services/" element={<Services />} />
<Route path=":services/:service/" element={<Services />} />
<Route path=":services/:service/:id" element={<Services />} />
</Routes>
<h4 className="underline">
<I18n path="settings.interface.title" />