Ditching load_queue flags, instead using simple loading prop on item itself (less capable but greater performance and predictability) and fixes #774

This commit is contained in:
James Barnsley
2021-11-28 15:55:09 +13:00
parent 713b30690b
commit 46e6bc4743
14 changed files with 495 additions and 529 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -215,6 +215,14 @@ export function removeFromLibrary(uri, itemUri) {
};
}
export function setLoading(uri, loading) {
return {
type: 'SET_LOADING',
uri,
loading,
};
}
/**
* Record loaders
*

View File

@ -360,6 +360,7 @@ const CoreMiddleware = (function () {
options,
});
});
next(action);
break;
}
@ -649,8 +650,10 @@ const CoreMiddleware = (function () {
case 'QUEUE_LOADED':
store.dispatch(coreActions.tracksLoaded(action.tracks));
action.tracks = formatTracks(action.tracks);
next(action);
next({
...action,
tracks: formatTracks(action.tracks),
});
break;
case 'ITEMS_LOADED':
@ -658,11 +661,12 @@ const CoreMiddleware = (function () {
action.items.forEach((item) => {
mergedItems.push({
...core.items[item.uri] || {},
loading: false, // Action can still override this
...item,
});
});
store.dispatch(uiActions.stopLoading(arrayOf('uri', action.items)));
store.dispatch(uiActions.stopLoading(arrayOf('uri', action.items))); // TODO Deprecate
store.dispatch(coreActions.updateColdStore(mergedItems));
next({
...action,

View File

@ -60,6 +60,32 @@ export default function reducer(core = {}, action) {
* These actions are only ever called by middleware after we've digested one more many assets
* and appended to their relevant index.
* */
case 'LOAD_ITEMS': {
const items = { ...core.items };
action.uris.forEach((uri) => {
items[uri] = {
...core.items[uri] || { uri },
loading: true,
};
});
return {
...core,
items,
};
}
case 'SET_LOADING': {
return {
...core,
items: {
...core.items,
[action.uri]: {
...core.items[action.uri] || { uri: action.uri },
loading: action.loading,
},
},
};
}
case 'ITEM_LOADED':
return {
@ -101,17 +127,14 @@ export default function reducer(core = {}, action) {
};
}
case 'USER_PLAYLISTS_LOADED':
var users = { ...core.users };
var existing_playlists_uris = [];
case 'USER_PLAYLISTS_LOADED': {
const users = { ...core.users };
let existing_playlists_uris = [];
if (users[action.uri] && users[action.uri].playlists_uris) {
existing_playlists_uris = users[action.uri].playlists_uris;
existing_playlists_uris = users[action.uri].playlists_uris;
}
var playlists_uris = [...existing_playlists_uris, ...arrayOf('uri', action.playlists)];
var user = {
const playlists_uris = [...existing_playlists_uris, ...arrayOf('uri', action.playlists)];
const user = {
...users[action.uri],
playlists_uris,
playlists_more: action.more,
@ -119,6 +142,7 @@ export default function reducer(core = {}, action) {
};
users[action.uri] = user;
return { ...core, users };
}
case 'RESTORE_LIBRARY_FROM_COLD_STORE':
const libraries = { ...core.libraries };

View File

@ -1242,6 +1242,12 @@ const MopidyMiddleware = (function () {
type: 'playlist',
});
if (response.tracks && fetchTracks) {
store.dispatch(coreActions.itemLoaded({
...playlist,
tracks: undefined,
loading: 'tracks',
}));
request(store, 'library.lookup', { uris: arrayOf('uri', response.tracks) })
.then((tracksResponse) => {
const tracks = response.tracks.map((simpleTrack) => {

View File

@ -950,7 +950,10 @@ export function getArtist(uri, { full, forceRefetch } = {}) {
}).then(
(response) => {
const artist = formatArtist(response);
dispatch(coreActions.itemLoaded(artist));
dispatch(coreActions.itemLoaded({
...artist,
loading: full ? 'albums' : false,
}));
dispatch(lastfmActions.getArtist(uri, artist.name, artist.mbid));
},
);
@ -959,16 +962,20 @@ export function getArtist(uri, { full, forceRefetch } = {}) {
if (full) {
// All albums (gets all pages, may take some time to iterate them all)
let albums = [];
let loading = 'albums';
const fetchAlbums = (endpoint) => request({
dispatch, getState, endpoint, uri,
})
.then((response) => {
albums = [...albums, ...formatAlbums(response.items)];
if (response.next) {
loading = 'albums';
fetchAlbums(`${response.next}${forceRefetch ? `&refetch=${Date.now()}` : ''}`);
} else {
loading = false;
dispatch(coreActions.itemLoaded({
uri,
loading,
albums_uris: arrayOf('uri', albums),
}));
dispatch(coreActions.itemsLoaded(albums));
@ -987,6 +994,7 @@ export function getArtist(uri, { full, forceRefetch } = {}) {
(response) => {
dispatch(coreActions.itemLoaded({
uri,
loading,
tracks: formatTracks(response.tracks),
}));
},
@ -1002,6 +1010,7 @@ export function getArtist(uri, { full, forceRefetch } = {}) {
(response) => {
dispatch(coreActions.itemLoaded({
uri,
loading,
related_artists: formatArtists(response.artists),
}));
},
@ -1389,6 +1398,7 @@ export function getPlaylist(uri, options = {}) {
// to accurately identify whether we've loaded *ALL* the tracks. Without this, it
// doesn't know if we've loaded all tracks, or just the first page.
tracks: null,
loading: full ? 'tracks' : null,
}));
if (full) {

View File

@ -315,6 +315,7 @@ const formatAlbum = function (data) {
const album = { type: 'album' };
const fields = [
'uri',
'loading',
'in_library',
'provider',
'name',
@ -388,6 +389,7 @@ const formatArtist = function (data) {
const artist = { type: 'artist' };
const fields = [
'uri',
'loading',
'in_library',
'provider',
'mbid',
@ -448,6 +450,7 @@ const formatPlaylist = function (data) {
const playlist = { type: 'playlist' };
const fields = [
'uri',
'loading',
'in_library',
'snapshot_id',
'provider',
@ -535,6 +538,7 @@ const formatUser = function (data) {
const user = { type: 'user' };
const fields = [
'id',
'loading',
'in_library',
'uri',
'provider',
@ -589,8 +593,8 @@ const formatUser = function (data) {
const formatTrack = function (data) {
const track = { type: 'track' };
const fields = [
'loading',
'uri',
'loading',
'in_library',
'is_playable',
'tlid',

View File

@ -90,6 +90,7 @@ const ensureLoaded = ({
// Forced refetch bypasses everything
if (forceRefetch) {
console.info(`Force-refetching "${uri}"`);
store.dispatch(coreActions.setLoading(uri, true));
fetch();
return;
}
@ -125,6 +126,7 @@ const ensureLoaded = ({
// What about in the coldstore?
localForage.getItem(uri).then((restoredItem) => {
if (!restoredItem || missingDependents(restoredItem).length > 0) {
store.dispatch(coreActions.setLoading(uri, true));
fetch();
return;
}

View File

@ -12,6 +12,7 @@ const getLibraries = (state) => state.core.libraries;
const getSearchResults = (state) => state.core.search_results;
const getGridGlowEnabled = (state) => state.ui.grid_glow_enabled;
const getDragger = (state) => state.ui.dragger;
const getSorts = (state) => state.ui.sort;
const makeItemSelector = (uri) => createSelector(
[getItems],
@ -192,6 +193,14 @@ const makeProvidersSelector = (context) => createSelector(
return providers[context].filter((p) => schemes.indexOf(p.scheme) > -1);
},
);
const makeSortSelector = (key, defaultField = 'sort_id') => createSelector(
[getSorts],
(sorts) => [
sorts[key]?.field || defaultField,
sorts[key]?.reverse || false,
],
);
const getSortSelector = (state, key, defaultField = 'sort_id') => {
const result = state.ui.sort[key];
return [
@ -215,5 +224,6 @@ export {
queueHistorySelector,
makeProvidersSelector,
getProvider,
makeSortSelector,
getSortSelector,
};

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import sanitizeHtml from 'sanitize-html';
import ErrorMessage from '../components/ErrorMessage';
import TrackList from '../components/TrackList';
@ -18,41 +18,37 @@ 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 spotifyActions from '../services/spotify/actions';
import * as lastfmActions from '../services/lastfm/actions';
import { uriSource } from '../util/helpers';
import Button from '../components/Button';
import { makeLoadingSelector, makeItemSelector, getSortSelector } from '../util/selectors';
import { makeItemSelector, makeSortSelector } from '../util/selectors';
import { applyFilter, sortItems } from '../util/arrays';
import { decodeUri } from '../util/format';
const SORT_KEY = 'album_tracks';
const Album = ({
uri,
album: albumProp,
sortField,
sortReverse,
loading,
slim_mode,
coreActions: {
loadAlbum,
},
lastfmActions: {
getAlbum,
},
uiActions: {
const Album = () => {
const { loadAlbum } = coreActions;
const { getAlbum } = lastfmActions;
const { playURIs } = mopidyActions;
const {
setSort,
setWindowTitle,
showContextMenu,
hideContextMenu,
},
mopidyActions: {
playURIs,
},
}) => {
} = uiActions;
const dispatch = useDispatch();
const { uri: encodedUri } = useParams();
const uri = decodeUri(encodedUri);
const itemSelector = makeItemSelector(uri);
const album = useSelector(itemSelector);
const sortSelector = makeSortSelector(SORT_KEY, 'disc_track');
const [sortField, sortReverse] = useSelector(sortSelector);
const slim_mode = useSelector((state) => state.ui.slim_mode);
const [filter, setFilter] = useState('');
const [album, setAlbum] = useState({});
let tracks = album?.tracks || [];
if (sortField && tracks) tracks = sortItems(tracks, sortField, sortReverse);
@ -60,21 +56,16 @@ const Album = ({
useEffect(
() => {
if (uri) {
loadAlbum(uri, { full: true });
}
if (uri) dispatch(loadAlbum(uri, { full: true }));
},
[uri],
);
useEffect(() => {
if (albumProp && !album) {
if (albumProp.artists && albumProp.wiki === undefined) {
getAlbum(albumProp.uri, albumProp.artists[0].name, albumProp.name);
}
};
setAlbum(albumProp);
}, [albumProp]);
if (album?.artists && album?.wiki === undefined) {
dispatch(getAlbum(album.uri, album.artists[0].name, album.name));
}
}, [album]);
useEffect(() => {
if (album) {
@ -87,7 +78,7 @@ const Album = ({
}
}, [album]);
if (loading) {
if (album?.loading) {
return <Loader body loading />;
}
@ -101,12 +92,14 @@ const Album = ({
);
}
const handleContextMenu = (e) => showContextMenu({
e,
context: 'album',
items: [album],
uris: [uri],
});
const handleContextMenu = (e) => dispatch(
showContextMenu({
e,
context: 'album',
items: [album],
uris: [uri],
}),
);
const onChangeSort = (field) => {
let reverse = false;
@ -114,8 +107,8 @@ const Album = ({
reverse = !sortReverse;
}
setSort(SORT_KEY, field, reverse);
hideContextMenu();
dispatch(setSort(SORT_KEY, field, reverse));
dispatch(hideContextMenu());
};
const sort_options = [
@ -189,7 +182,7 @@ const Album = ({
<div className="actions">
<Button
type="primary"
onClick={() => playURIs([uri], uri)}
onClick={() => dispatch(playURIs([uri], uri))}
tracking={{ category: 'Album', action: 'Play' }}
>
<I18n path="actions.play" />
@ -247,39 +240,4 @@ const Album = ({
);
}
const mapStateToProps = (state, ownProps) => {
const uri = decodeUri(ownProps.match.params.uri);
const itemSelector = makeItemSelector(uri);
const loadingSelector = makeLoadingSelector([`(.*)${uri}(.*)`, '^((?!contains).)*$', '^((?!me\/albums).)*$', '^((?!followers).)*$']);
const [sortField, sortReverse] = getSortSelector(state, SORT_KEY, 'disc_track');
return {
uri,
slim_mode: state.ui.slim_mode,
theme: state.ui.theme,
album: itemSelector(state),
loading: loadingSelector(state),
spotify_library_albums: state.spotify.library_albums,
local_library_albums: state.mopidy.library_albums,
spotify_authorized: state.spotify.authorization,
sortField,
sortReverse,
};
};
const mapDispatchToProps = (dispatch) => ({
coreActions: bindActionCreators(coreActions, dispatch),
uiActions: bindActionCreators(uiActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch),
spotifyActions: bindActionCreators(spotifyActions, dispatch),
lastfmActions: bindActionCreators(lastfmActions, dispatch),
});
export {
Album,
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Album);
export default Album;

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Route, Switch } from 'react-router-dom';
import { Route, Switch, useParams, useHistory } from 'react-router-dom';
import ErrorMessage from '../components/ErrorMessage';
import Link from '../components/Link';
import Thumbnail from '../components/Thumbnail';
@ -23,46 +23,43 @@ import { encodeUri, decodeUri } from '../util/format';
import { arrayOf } from '../util/arrays';
import { i18n, I18n } from '../locale';
import Button from '../components/Button';
import {
makeItemSelector,
makeLoadingSelector,
} from '../util/selectors';
import { makeItemSelector } from '../util/selectors';
const Artist = ({
uri,
loading,
history,
artist: artistProp,
albums,
coreActions: {
loadArtist,
},
uiActions: {
const Artist = () => {
const history = useHistory();
const dispatch = useDispatch();
const { uri: encodedUri } = useParams();
const uri = decodeUri(encodedUri);
const artistSelector = makeItemSelector(uri);
const artist = useSelector(artistSelector);
const albumSelector = makeItemSelector(artist?.albums_uris || []);
const albums = useSelector(albumSelector);
const loading = artist?.loading && artist.loading !== 'albums';
const { loadArtist } = coreActions;
const { playURIs } = mopidyActions;
const {
setWindowTitle,
showContextMenu,
createNotification,
},
mopidyActions: {
playURIs,
},
}) => {
const [artist, setArtist] = useState({});
} = uiActions;
useEffect(
() => {
if (uri) loadArtist(uri, { full: true });
if (uri) dispatch(loadArtist(uri, { full: true }));
},
[uri],
);
useEffect(() => {
if (artistProp) {
setWindowTitle(i18n('artist.title_window', { name: artistProp.name }));
if (artist) {
dispatch(setWindowTitle(i18n('artist.title_window', { name: artist.name })));
} else {
setWindowTitle(i18n('artist.title'));
dispatch(setWindowTitle(i18n('artist.title')));
}
setArtist(artistProp);
}, [artistProp]);
}, [artist]);
const onPlayAll = () => {
const {
@ -71,20 +68,22 @@ const Artist = ({
} = artist;
if ((!albums_uris || !albums_uris.length) && (!tracks || !tracks.length)) {
createNotification({ content: i18n('errors.no_results'), level: 'warning' });
dispatch(createNotification({ content: i18n('errors.no_results'), level: 'warning' }));
return;
}
playURIs(arrayOf('uri', tracks) || albums_uris, uri);
}
dispatch(playURIs(arrayOf('uri', tracks) || albums_uris, uri));
};
const handleContextMenu = (e) => showContextMenu({
e,
context: 'artist',
items: [artist],
uris: [uri],
tracklist_uri: uri,
});
const handleContextMenu = (e) => dispatch(
showContextMenu({
e,
context: 'artist',
items: [artist],
uris: [uri],
tracklist_uri: uri,
}),
);
if (loading) {
return <Loader body loading />;
@ -202,32 +201,4 @@ const Artist = ({
);
}
const mapStateToProps = (state, props) => {
const uri = decodeUri(props.match.params.uri);
const loadingSelector = makeLoadingSelector([`(.*)${uri}(.*)`, '^((?!contains).)*$', '^((?!albums).)*$', '^((?!related-artists).)*$', '^((?!top-tracks).)*$', '^((?!following).)*$']);
const artistSelector = makeItemSelector(uri);
const artist = artistSelector(state);
let albums = null;
if (artist && artist.albums_uris) {
const albumsSelector = makeItemSelector(artist.albums_uris);
albums = albumsSelector(state);
}
return {
uri,
artist,
albums,
loading: loadingSelector(state),
theme: state.ui.theme,
slim_mode: state.ui.slim_mode,
spotify_authorized: state.spotify.authorization,
};
};
const mapDispatchToProps = (dispatch) => ({
coreActions: bindActionCreators(coreActions, dispatch),
uiActions: bindActionCreators(uiActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Artist);
export default Artist;

View File

@ -9,11 +9,12 @@ import RelatedArtists from '../../components/RelatedArtists';
import { i18n, I18n } from '../../locale';
import { sortItems, applyFilter } from '../../util/arrays';
import { encodeUri } from '../../util/format';
import { getSortSelector } from '../../util/selectors';
import { makeSortSelector } from '../../util/selectors';
import {
hideContextMenu,
setSort,
} from '../../services/ui/actions';
import Loader from '../../components/Loader';
const SORT_KEY = 'artist_albums';
@ -23,16 +24,16 @@ export default ({
}) => {
const dispatch = useDispatch();
const [search, setSearch] = useState('');
const [type, setType] = useState('');
const [sortField, sortReverse] = useSelector(
(state) => getSortSelector(state, SORT_KEY, null),
);
const [type, setType] = useState(null);
const sortSelector = makeSortSelector(SORT_KEY, null);
const [sortField, sortReverse] = useSelector(sortSelector);
const {
uri,
tracks,
related_artists,
} = artist;
let albums = albumsProp;
const loadingAlbums = artist?.loading && artist.loading === 'albums';
const onSortChange = (field) => {
let reverse = false;
@ -125,6 +126,7 @@ export default ({
<div className="albums">
<h4>
<I18n path="artist.overview.albums" count={albums ? albums.length : 0} />
{loadingAlbums && <Loader loading mini />}
<div className="actions-wrapper">
<FilterField
initialValue={search}

View File

@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { useParams, useHistory } from 'react-router-dom';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators } from 'redux';
import ErrorMessage from '../components/ErrorMessage';
import Button from '../components/Button';
@ -21,7 +22,7 @@ import * as mopidyActions from '../services/mopidy/actions';
import * as spotifyActions from '../services/spotify/actions';
import { uriSource } from '../util/helpers';
import { i18n, I18n } from '../locale';
import { makeItemSelector, makeLoadingSelector, getSortSelector } from '../util/selectors';
import { makeItemSelector, makeSortSelector } from '../util/selectors';
import { sortItems, applyFilter } from '../util/arrays';
import { decodeUri, encodeUri } from '../util/format';
@ -118,16 +119,7 @@ const Actions = ({
}
const Playlist = ({
loading,
loading_tracks,
slim_mode,
history,
uri,
encodedUri,
name,
playlist: playlistProp,
sortField,
sortReverse,
coreActions: {
loadPlaylist,
reorderPlaylistTracks,
@ -144,8 +136,16 @@ const Playlist = ({
playPlaylist,
},
}) => {
const [playlist, setPlaylist] = useState({});
const history = useHistory();
const { uri: encodedUri, name } = useParams();
const uri = decodeUri(encodedUri);
const [filter, setFilter] = useState('');
const sortSelector = makeSortSelector(SORT_KEY);
const [sortField, sortReverse] = useSelector(sortSelector);
const itemSelector = makeItemSelector(uri);
const playlist = useSelector(itemSelector);
const loading = playlist?.loading && playlist?.loading !== 'tracks';
const loadingTracks = playlist?.loading === 'tracks';
useEffect(
() => {
@ -155,19 +155,18 @@ const Playlist = ({
);
useEffect(() => {
if (playlistProp && playlist && playlistProp.moved_to !== playlist.moved_to) {
history.push(`/playlist/${encodeUri(playlistProp.moved_to)}`);
if (playlist && playlist.moved_to) {
history.push(`/playlist/${encodeUri(playlist.moved_to)}/${encodeURIComponent(playlist.name.replace('%', ''))}`);
}
}, [playlistProp])
}, [playlist]);
useEffect(() => {
if (playlistProp) {
setWindowTitle(i18n('playlist.title_window', { name: playlistProp.name }));
if (playlist) {
setWindowTitle(i18n('playlist.title_window', { name: playlist.name }));
} else {
setWindowTitle(i18n('playlist.title'));
}
setPlaylist(playlistProp);
}, [playlistProp]);
}, [playlist]);
const handleContextMenu = (e) => showContextMenu({
e,
@ -312,7 +311,7 @@ const Playlist = ({
<h4 className="no-bottom-margin">
<I18n path="playlist.tracks.title" />
{loading_tracks && <Loader loading mini />}
{loadingTracks && <Loader loading mini />}
<div className="actions-wrapper">
<FilterField
initialValue={filter}
@ -362,28 +361,15 @@ const mapStateToProps = (state, ownProps) => {
} = {},
} = state;
const uri = decodeUri(ownProps.match.params.uri);
const itemSelector = makeItemSelector(uri);
const loadingSelector = makeLoadingSelector([`(.*)${uri}(.*)`, '^((?!contains).)*$', '^((?!tracks).)*$', '^((?!followers).)*$']);
const loadingTracksSelector = makeLoadingSelector([`(.*)${uri}(.*)tracks(.*)`]);
const [sortField, sortReverse] = getSortSelector(state, SORT_KEY);
return {
uri,
encodedUri: ownProps.match.params.uri,
name: ownProps.match.params.name,
allow_reporting,
slim_mode,
theme,
loading: loadingSelector(state),
loading_tracks: loadingTracksSelector(state),
playlist: itemSelector(state),
spotify_library_playlists,
local_library_playlists,
spotify_authorized,
spotify_userid: (me && me.id) || null,
sortField,
sortReverse,
};
};