Upgrading BrowseDirectory to functional component

This commit is contained in:
James Barnsley
2023-01-25 19:56:11 +13:00
parent 58f487c2e2
commit b99894901a
2 changed files with 168 additions and 231 deletions

View File

@ -15,10 +15,15 @@ import { SourceIcon } from '../components/Icon';
import DropdownField from '../components/Fields/DropdownField';
import FilterField from '../components/Fields/FilterField';
import { i18n, I18n } from '../locale';
import * as coreActions from '../services/core/actions';
import * as uiActions from '../services/ui/actions';
import * as mopidyActions from '../services/mopidy/actions';
import * as lastfmActions from '../services/lastfm/actions';
import { loadAlbum } from '../services/core/actions';
import { playURIs } from '../services/mopidy/actions';
import { getAlbum } from '../services/lastfm/actions';
import {
setSort,
setWindowTitle,
showContextMenu,
hideContextMenu,
} from '../services/ui/actions';
import { uriSource } from '../util/helpers';
import Button from '../components/Button';
import { makeItemSelector, makeSortSelector } from '../util/selectors';
@ -28,15 +33,6 @@ import { decodeUri, formatContext } from '../util/format';
const SORT_KEY = 'album_tracks';
const Album = () => {
const { loadAlbum } = coreActions;
const { getAlbum } = lastfmActions;
const { playURIs } = mopidyActions;
const {
setSort,
setWindowTitle,
showContextMenu,
hideContextMenu,
} = uiActions;
const dispatch = useDispatch();
const { uri: encodedUri } = useParams();
const uri = decodeUri(encodedUri);

View File

@ -1,6 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom';
import Loader from '../../components/Loader';
import Header from '../../components/Header';
import TrackList from '../../components/TrackList';
@ -10,8 +10,15 @@ import DropdownField from '../../components/Fields/DropdownField';
import FilterField from '../../components/Fields/FilterField';
import Icon from '../../components/Icon';
import ErrorBoundary from '../../components/ErrorBoundary';
import * as uiActions from '../../services/ui/actions';
import * as mopidyActions from '../../services/mopidy/actions';
import {
set,
setWindowTitle,
hideContextMenu,
} from '../../services/ui/actions';
import {
getDirectory,
playURIs,
} from '../../services/mopidy/actions';
import * as spotifyActions from '../../services/spotify/actions';
import { arrayOf, sortItems, applyFilter } from '../../util/arrays';
import { i18n, I18n } from '../../locale';
@ -19,7 +26,8 @@ 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 loadingSelector = makeLoadingSelector(['mopidy_library.(browse|lookup)']);
const Breadcrumbs = ({ uri }) => {
let parent_uri = uri || null;
@ -64,221 +72,154 @@ const Subdirectories = ({ items, view }) => {
);
};
class BrowseDirectory extends React.Component {
constructor(props) {
super(props);
const BrowseDirectory = () => {
const dispatch = useDispatch();
const [filter, setFilter] = useState('');
const directoryInState = useSelector(({ mopidy }) => mopidy?.directory || {});
const view = useSelector(({ ui }) => ui?.library_directory_view);
const loading = useSelector(loadingSelector);
this.state = {
filter: '',
};
}
componentDidMount() {
const {
uiActions: {
setWindowTitle,
},
} = this.props;
setWindowTitle(i18n('library.browse_directory.title'));
this.loadDirectory();
}
componentDidUpdate = ({ uri: prevUri }) => {
const { uri } = this.props;
if (uri && uri !== prevUri) {
this.loadDirectory();
}
}
loadDirectory = () => {
const {
uri,
mopidyActions: {
getDirectory,
},
} = this.props;
getDirectory(uri);
}
playAll = () => {
const {
uri,
mopidyActions: {
playURIs,
},
uiActions: {
hideContextMenu,
},
directory: {
tracks,
} = {},
} = this.props;
if (!tracks || !tracks.length) return;
playURIs({
uris: arrayOf('uri', sortItems(tracks, 'name')),
from: {
name: 'Browse',
type: 'browse',
uri,
},
});
hideContextMenu();
}
goBack = () => {
const { uiActions: { hideContextMenu } } = this.props;
window.history.back();
hideContextMenu();
}
render = () => {
const {
uri,
directory,
uiActions,
loading,
view,
name,
} = this.props;
const { filter } = this.state;
if (!directory || (!directory.subdirectories && !directory.tracks)) {
if (loading) {
return <Loader body loading />;
}
return (
<ErrorMessage type="not-found" title="Not found">
<p>
<I18n path="errors.uri_not_found" uri={uri} />
</p>
</ErrorMessage>
);
}
let subdirectories = directory?.subdirectories;
let tracks = directory?.tracks;
subdirectories = sortItems(subdirectories, 'name');
tracks = sortItems(tracks, 'name');
if (filter && filter !== '') {
subdirectories = applyFilter('name', filter, subdirectories);
tracks = applyFilter('name', filter, tracks);
}
const view_options = [
{
value: 'thumbnails',
label: i18n('fields.filters.thumbnails'),
},
{
value: 'list',
label: i18n('fields.filters.list'),
},
];
const options = (
<>
<FilterField
initialValue={filter}
handleChange={(value) => this.setState({ filter: value })}
onSubmit={() => uiActions.hideContextMenu()}
/>
<DropdownField
icon="visibility"
name="View"
value={view}
valueAsLabel
options={view_options}
handleChange={(value) => { uiActions.set({ library_directory_view: value }); uiActions.hideContextMenu(); }}
/>
{tracks && (
<Button
onClick={this.playAll}
noHover
discrete
tracking={{ category: 'Directory', action: 'Play' }}
>
<Icon name="play_circle_filled" />
<I18n path="actions.play_all" />
</Button>
)}
<Button
onClick={(e) => { uiActions.hideContextMenu(); this.goBack(e); }}
noHover
tracking={{ category: 'Directory', action: 'Back' }}
>
<Icon name="keyboard_backspace" />
<I18n path="actions.back" />
</Button>
</>
);
return (
<div className="view library-local-view">
<Header options={options} uiActions={uiActions}>
<Icon name="folder" type="material" />
<div className="header__text">
{name || i18n('library.browse_directory.title')}
<Breadcrumbs uri={uri} />
</div>
</Header>
<section className="content-wrapper">
<ErrorBoundary>
<Subdirectories items={subdirectories} view={view} />
<TrackList
context={{
uri,
name: 'Browse',
type: 'browse',
}}
tracks={tracks}
className="library-local-track-list"
/>
</ErrorBoundary>
</section>
</div>
);
}
}
const loadingSelector = makeLoadingSelector(['mopidy_library.(browse|lookup)']);
const mapStateToProps = (state, ownProps) => {
console.debug('browser directory', ownProps)
const {
mopidy: {
directory: _directory = {},
},
ui: {
library_directory_view: view,
},
} = state;
const uri = decodeUri(ownProps.params.uri);
let { uri, name } = useParams();
uri = decodeUri(uri);
name = decodeURIComponent(name);
const uriMatcher = [uri, decodeURIComponent(uri)]; // Lenient matching due to encoding diffs
const directory = _directory && uriMatcher.includes(_directory.uri)
? _directory
const directory = directoryInState && uriMatcher.includes(directoryInState.uri)
? directoryInState
: undefined;
return {
uri,
name: decodeURIComponent(ownProps.params.name),
loading: loadingSelector(state),
directory,
view,
};
};
useEffect(() => {
dispatch(setWindowTitle(i18n('library.browse_directory.title')));
dispatch(getDirectory(uri));
}, []);
const mapDispatchToProps = (dispatch) => ({
uiActions: bindActionCreators(uiActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch),
spotifyActions: bindActionCreators(spotifyActions, dispatch),
});
useEffect(() => {
dispatch(getDirectory(uri));
}, [uri]);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(BrowseDirectory));
const onViewChange = (value) => {
dispatch(set({ library_directory_view: value }));
dispatch(hideContextMenu());
}
const playAll = () => {
if (!tracks || !tracks.length) return;
dispatch(
playURIs({
uris: arrayOf('uri', sortItems(tracks, 'name')),
from: {
name: 'Browse',
type: 'browse',
uri,
},
})
);
dispatch(hideContextMenu());
}
const goBack = () => {
dispatch(hideContextMenu());
window.history.back();
}
if (!directory || (!directory.subdirectories && !directory.tracks)) {
if (loading) {
return <Loader body loading />;
}
return (
<ErrorMessage type="not-found" title="Not found">
<p>
<I18n path="errors.uri_not_found" uri={uri} />
</p>
</ErrorMessage>
);
}
let subdirectories = directory?.subdirectories;
let tracks = directory?.tracks;
subdirectories = sortItems(subdirectories, 'name');
tracks = sortItems(tracks, 'name');
if (filter && filter !== '') {
subdirectories = applyFilter('name', filter, subdirectories);
tracks = applyFilter('name', filter, tracks);
}
const view_options = [
{
value: 'thumbnails',
label: i18n('fields.filters.thumbnails'),
},
{
value: 'list',
label: i18n('fields.filters.list'),
},
];
const options = (
<>
<FilterField
initialValue={filter}
handleChange={setFilter}
onSubmit={() => dispatch(hideContextMenu())}
/>
<DropdownField
icon="visibility"
name="View"
value={view}
valueAsLabel
options={view_options}
handleChange={onViewChange}
/>
{tracks && (
<Button
onClick={playAll}
noHover
discrete
tracking={{ category: 'Directory', action: 'Play' }}
>
<Icon name="play_circle_filled" />
<I18n path="actions.play_all" />
</Button>
)}
<Button
onClick={goBack}
tracking={{ category: 'Directory', action: 'Back' }}
noHover
>
<Icon name="keyboard_backspace" />
<I18n path="actions.back" />
</Button>
</>
);
return (
<div className="view library-local-view">
<Header options={options}>
<Icon name="folder" type="material" />
<div className="header__text">
{name || i18n('library.browse_directory.title')}
<Breadcrumbs uri={uri} />
</div>
</Header>
<section className="content-wrapper">
<ErrorBoundary>
<Subdirectories items={subdirectories} view={view} />
<TrackList
context={{
uri,
name: 'Browse',
type: 'browse',
}}
tracks={tracks}
className="library-local-track-list"
/>
</ErrorBoundary>
</section>
</div>
);
}
export default BrowseDirectory;