Refactoring Featured Playlists for multi-source support
This commit is contained in:
@ -6,7 +6,6 @@ const Grid = memo(({
|
||||
items,
|
||||
className = '',
|
||||
mini,
|
||||
tile,
|
||||
getLink,
|
||||
sourceIcon,
|
||||
}) => {
|
||||
@ -14,13 +13,12 @@ const Grid = memo(({
|
||||
|
||||
return (
|
||||
<SmartList
|
||||
className={`grid grid--${items[0].type}s ${className} ${mini ? 'grid--mini' : ''} ${tile ? 'grid--tile' : ''}`}
|
||||
className={`grid grid--${items[0].type}s ${className} ${mini ? 'grid--mini' : ''}`}
|
||||
items={items}
|
||||
itemComponent={GridItem}
|
||||
itemProps={{
|
||||
getLink,
|
||||
sourceIcon,
|
||||
tile,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -78,6 +78,7 @@ const GridItem = ({
|
||||
type: item?.type?.toUpperCase() || 'UNKNOWN',
|
||||
item: { item, context: item },
|
||||
});
|
||||
const tile = ['playlist_group', 'mood', 'directory', 'category'].indexOf(item?.type) > -1
|
||||
|
||||
const onContextMenu = (e) => {
|
||||
e.preventDefault();
|
||||
@ -127,7 +128,7 @@ const GridItem = ({
|
||||
return (
|
||||
<div
|
||||
ref={isTouchDevice() ? undefined : drag}
|
||||
className={`grid__item grid__item--${itemProp.type}`}
|
||||
className={`grid__item grid__item--${itemProp.type} ${tile ? 'grid__item--tile' : ''}`}
|
||||
>
|
||||
<Link
|
||||
to={to}
|
||||
|
||||
@ -104,7 +104,7 @@ const Sidebar = () => {
|
||||
<I18n path="sidebar.moods" />
|
||||
</Link>
|
||||
{spotify_available && (
|
||||
<Link to="/discover/featured" className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
|
||||
<Link to="/discover/featured-playlists" className="sidebar__menu__item" activeClassName="sidebar__menu__item--active">
|
||||
<Icon name="star" type="material" />
|
||||
<I18n path="sidebar.featured_playlists" />
|
||||
</Link>
|
||||
|
||||
@ -138,6 +138,7 @@ services:
|
||||
searching: 'Searching %{provider} %{type}'
|
||||
searching_providers: 'Searching %{count} Mopidy providers'
|
||||
loading_albums: 'Loading %{count} local albums'
|
||||
loading_artwork: 'Loading artwork'
|
||||
spotify:
|
||||
title: Spotify
|
||||
pusher:
|
||||
@ -484,7 +485,7 @@ discover:
|
||||
title: Discover
|
||||
moods:
|
||||
title: 'Genre / Mood'
|
||||
featured:
|
||||
featured_playlists:
|
||||
title: Featured playlists
|
||||
new_releases:
|
||||
title: New releases
|
||||
|
||||
@ -549,6 +549,7 @@ const CoreMiddleware = (function () {
|
||||
|
||||
case 'LOAD_LIBRARY':
|
||||
store.dispatch(uiActions.startLoading(action.uri, action.uri));
|
||||
console.debug(action);
|
||||
const fetchLibrary = () => {
|
||||
switch (uriSource(action.uri)) {
|
||||
case 'spotify':
|
||||
|
||||
@ -484,6 +484,13 @@ export function getLibraryMoods(uri) {
|
||||
};
|
||||
}
|
||||
|
||||
export function getLibraryFeaturedPlaylists(uri) {
|
||||
return {
|
||||
type: 'MOPIDY_GET_LIBRARY_FEATURED_PLAYLISTS',
|
||||
uri,
|
||||
};
|
||||
}
|
||||
|
||||
export function getLibraryPlaylists(uri) {
|
||||
return {
|
||||
type: 'MOPIDY_GET_LIBRARY_PLAYLISTS',
|
||||
|
||||
@ -1899,7 +1899,7 @@ const MopidyMiddleware = (function () {
|
||||
} = response;
|
||||
|
||||
// Not all endpoints give us tracks/subdirectories to library.lookup
|
||||
if (!results.length || uri.startsWith('file:')) {
|
||||
if (!results.length || uri.startsWith('file:') || uri.startsWith('ytmusic:')) {
|
||||
console.info(`No 'library.lookup' results for ${uri}, trying 'library.browse'`);
|
||||
getBrowse();
|
||||
} else {
|
||||
@ -1985,37 +1985,107 @@ const MopidyMiddleware = (function () {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'MOPIDY_GET_LIBRARY_FEATURED_PLAYLISTS': {
|
||||
store.dispatch(uiActions.startProcess(action.type, { notification: false }));
|
||||
|
||||
request(store, 'library.browse', { uri: action.uri })
|
||||
.then((response) => {
|
||||
const moods = response.map((mood) => ({
|
||||
...formatSimpleObject(mood),
|
||||
type: 'playlist_group',
|
||||
}));
|
||||
|
||||
store.dispatch(
|
||||
uiActions.updateProcess(
|
||||
action.type,
|
||||
{
|
||||
total: moods.length,
|
||||
remaining: moods.length,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
store.dispatch(coreActions.libraryLoaded({
|
||||
uri: action.uri,
|
||||
type: 'featured_playlists',
|
||||
items_uris: arrayOf('uri', moods),
|
||||
}));
|
||||
store.dispatch(coreActions.itemsLoaded(moods));
|
||||
store.dispatch(uiActions.stopLoading(action.uri));
|
||||
store.dispatch(uiActions.processFinished(action.type));
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'MOPIDY_GET_PLAYLIST_GROUP': {
|
||||
const processKey = `playlist_group_${action.uri}`;
|
||||
const decodedUri = action.uri ? decodeURIComponent(action.uri) : null;
|
||||
const playlistGroup = formatPlaylistGroup({
|
||||
uri: decodedUri,
|
||||
loading: true,
|
||||
});
|
||||
store.dispatch(uiActions.startProcess(
|
||||
processKey,
|
||||
{ content: i18n('services.mopidy.loading_artwork') },
|
||||
));
|
||||
store.dispatch(coreActions.itemLoaded(playlistGroup));
|
||||
request(store, 'library.browse', { uri: action.uri })
|
||||
.then((browse) => {
|
||||
const playlists = formatPlaylists(browse);
|
||||
const playlists = browse.map((item) => formatPlaylist({
|
||||
...item,
|
||||
images: [], // Images is a playlist dependency, so this prevents triggering full load
|
||||
}));
|
||||
const playlists_uris = arrayOf('uri', playlists);
|
||||
request(store, 'library.getImages', { uris: playlists_uris })
|
||||
.then((response) => {
|
||||
const playlistsWithImages = playlists.map((playlist) => {
|
||||
let images = response[playlist.uri] || [];
|
||||
if (images) {
|
||||
images = formatImages(digestMopidyImages(store.getState().mopidy, images));
|
||||
}
|
||||
return {
|
||||
...playlist,
|
||||
images,
|
||||
};
|
||||
});
|
||||
const allUris = [...playlists_uris];
|
||||
store.dispatch(coreActions.itemLoaded({
|
||||
...playlistGroup,
|
||||
loading: false,
|
||||
playlists_uris,
|
||||
}));
|
||||
store.dispatch(coreActions.itemsLoaded(playlists));
|
||||
store.dispatch(
|
||||
uiActions.updateProcess(
|
||||
processKey,
|
||||
{
|
||||
total: allUris.length,
|
||||
remaining: allUris.length,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
store.dispatch(coreActions.itemsLoaded(playlistsWithImages));
|
||||
store.dispatch(coreActions.itemLoaded({
|
||||
...playlistGroup,
|
||||
loading: false,
|
||||
playlists_uris,
|
||||
}));
|
||||
});
|
||||
const run = () => {
|
||||
if (allUris.length) {
|
||||
const uris = allUris.splice(0, 5);
|
||||
const processor = store.getState().ui.processes[processKey];
|
||||
if (processor && processor.status === 'cancelling') {
|
||||
store.dispatch(uiActions.processCancelled(processKey));
|
||||
return;
|
||||
}
|
||||
store.dispatch(uiActions.updateProcess(processKey, { remaining: allUris.length }));
|
||||
|
||||
request(store, 'library.getImages', { uris })
|
||||
.then(
|
||||
(response) => {
|
||||
const withImages = uris.map((uri) => {
|
||||
let images = response[uri] || [];
|
||||
if (images) {
|
||||
images = formatImages(digestMopidyImages(store.getState().mopidy, images));
|
||||
}
|
||||
return {
|
||||
uri,
|
||||
images,
|
||||
};
|
||||
});
|
||||
store.dispatch(coreActions.itemsLoaded(withImages));
|
||||
run();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
store.dispatch(uiActions.processFinished(processKey));
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@ -362,9 +362,10 @@ export function getTrack(uri, { forceRefetch, full, lyrics }) {
|
||||
};
|
||||
}
|
||||
|
||||
export function getFeaturedPlaylists(forceRefetch = false) {
|
||||
export function getLibraryFeaturedPlaylists(forceRefetch = false) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: 'SPOTIFY_FEATURED_PLAYLISTS_LOADED', data: false });
|
||||
const processKey = 'SPOTIFY_GET_LIBRARY_FEATURED_PLAYLISTS';
|
||||
dispatch({ type: processKey, data: false });
|
||||
|
||||
const date = new Date();
|
||||
date.setHours(date.getHours());
|
||||
@ -402,9 +403,13 @@ export function getFeaturedPlaylists(forceRefetch = false) {
|
||||
);
|
||||
|
||||
dispatch(coreActions.itemsLoaded(playlists));
|
||||
|
||||
dispatch(coreActions.libraryLoaded({
|
||||
uri: 'spotify:featured',
|
||||
type: 'featured_playlists',
|
||||
items_uris: arrayOf('uri', playlists),
|
||||
}));
|
||||
dispatch({
|
||||
type: 'SPOTIFY_FEATURED_PLAYLISTS_LOADED',
|
||||
type: processKey,
|
||||
data: {
|
||||
message: response.message,
|
||||
uris: upgradeSpotifyPlaylistUris(arrayOf('uri', playlists)),
|
||||
|
||||
@ -205,6 +205,18 @@ const providers = {
|
||||
title: i18n('services.youtube.title'),
|
||||
},
|
||||
],
|
||||
featured_playlists: [
|
||||
{
|
||||
scheme: 'spotify:',
|
||||
uri: 'spotify:featured',
|
||||
title: i18n('services.spotify.title'),
|
||||
},
|
||||
{
|
||||
scheme: 'ytmusic:',
|
||||
uri: 'ytmusic:auto',
|
||||
title: i18n('services.youtube.title'),
|
||||
},
|
||||
],
|
||||
};
|
||||
const getProvider = (type, scheme) => providers[type]?.find((p) => p.scheme === scheme);
|
||||
const getUriSchemes = (state) => state.mopidy.uri_schemes || [];
|
||||
|
||||
@ -1,102 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import Header from '../../components/Header';
|
||||
import Icon from '../../components/Icon';
|
||||
import Button from '../../components/Button';
|
||||
import FilterField from '../../components/Fields/FilterField';
|
||||
import { Grid } from '../../components/Grid';
|
||||
import Loader from '../../components/Loader';
|
||||
import * as uiActions from '../../services/ui/actions';
|
||||
import * as spotifyActions from '../../services/spotify/actions';
|
||||
import { encodeUri } from '../../util/format';
|
||||
import { i18n, I18n } from '../../locale';
|
||||
import { indexToArray, applyFilter } from '../../util/arrays';
|
||||
import { makeLoadingSelector } from '../../util/selectors';
|
||||
|
||||
const Categories = ({
|
||||
loading,
|
||||
categories: categoriesProp,
|
||||
spotifyActions: {
|
||||
getCategories,
|
||||
},
|
||||
}) => {
|
||||
const {
|
||||
setWindowTitle,
|
||||
hideContextMenu,
|
||||
} = uiActions;
|
||||
const [filter, setFilter] = useState('');
|
||||
let categories = categoriesProp;
|
||||
|
||||
useEffect(() => {
|
||||
// Check for an empty category index, or where we've only got one loaded
|
||||
// This would be the case if you've refreshed from within a category and only loaded
|
||||
// the single record.
|
||||
if (!categories || Object.keys(categories).length <= 1) {
|
||||
getCategories();
|
||||
}
|
||||
setWindowTitle(i18n('discover.categories.title'));
|
||||
}, []);
|
||||
|
||||
const refresh = () => {
|
||||
hideContextMenu();
|
||||
getCategories();
|
||||
}
|
||||
|
||||
if (filter && filter !== '') categories = applyFilter('name', filter, categories);
|
||||
|
||||
const options = (
|
||||
<>
|
||||
<FilterField
|
||||
initialValue={filter}
|
||||
handleChange={setFilter}
|
||||
onSubmit={() => hideContextMenu()}
|
||||
/>
|
||||
<Button
|
||||
noHover
|
||||
onClick={refresh}
|
||||
tracking={{ category: 'DiscoverCategory', action: 'Refresh' }}
|
||||
>
|
||||
<Icon name="refresh" />
|
||||
<I18n path="actions.refresh" />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="view discover-categories-view">
|
||||
<Header uiActions={uiActions} options={options}>
|
||||
<Icon name="mood" type="material" />
|
||||
<I18n path="discover.categories.title" />
|
||||
</Header>
|
||||
<section className="content-wrapper grid-wrapper">
|
||||
{loading ? (
|
||||
<Loader body loading />
|
||||
) : (
|
||||
<Grid
|
||||
className="grid--tiles"
|
||||
items={categories}
|
||||
getLink={(item) => `/discover/categories/${encodeUri(item.uri)}`}
|
||||
sourceIcon={false}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const loadingSelector = makeLoadingSelector(['spotify_categories']);
|
||||
|
||||
return {
|
||||
loading: loadingSelector(state),
|
||||
categories: indexToArray(state.spotify.categories),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Categories);
|
||||
@ -1,173 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import Header from '../../components/Header';
|
||||
import DropdownField from '../../components/Fields/DropdownField';
|
||||
import FilterField from '../../components/Fields/FilterField';
|
||||
import Icon from '../../components/Icon';
|
||||
import { Grid } from '../../components/Grid';
|
||||
import Loader from '../../components/Loader';
|
||||
import ErrorMessage from '../../components/ErrorMessage';
|
||||
import Button from '../../components/Button';
|
||||
import * as uiActions from '../../services/ui/actions';
|
||||
import * as coreActions from '../../services/core/actions';
|
||||
import * as spotifyActions from '../../services/spotify/actions';
|
||||
import { I18n, i18n } from '../../locale';
|
||||
import {
|
||||
makeItemSelector,
|
||||
makeLoadingSelector,
|
||||
getSortSelector,
|
||||
} from '../../util/selectors';
|
||||
import { sortItems, applyFilter } from '../../util/arrays';
|
||||
import { decodeUri } from '../../util/format';
|
||||
|
||||
const SORT_KEY = 'discover_category';
|
||||
|
||||
const Category = ({
|
||||
uri,
|
||||
category,
|
||||
loading,
|
||||
playlists: playlistsProp,
|
||||
sortField,
|
||||
sortReverse,
|
||||
spotifyActions: {
|
||||
getCategory,
|
||||
},
|
||||
uiActions: {
|
||||
setSort,
|
||||
hideContextMenu,
|
||||
setWindowTitle,
|
||||
},
|
||||
}) => {
|
||||
const [filter, setFilter] = useState('');
|
||||
useEffect(() => {
|
||||
if (!category) getCategory(uri);
|
||||
}, [uri]);
|
||||
|
||||
useEffect(() => {
|
||||
if (category) {
|
||||
setWindowTitle(category.name);
|
||||
} else {
|
||||
setWindowTitle(i18n('discover.category.title'));
|
||||
}
|
||||
}, [category]);
|
||||
|
||||
const refresh = () => {
|
||||
hideContextMenu();
|
||||
getCategory(uri, { forceRefetch: true });
|
||||
}
|
||||
|
||||
const onSortChange = (field) => {
|
||||
let reverse = false;
|
||||
if (field !== null && sortField === field) {
|
||||
reverse = !sortReverse;
|
||||
}
|
||||
|
||||
setSort(SORT_KEY, field, reverse);
|
||||
hideContextMenu();
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loader body loading />;
|
||||
}
|
||||
|
||||
if (!category) {
|
||||
return (
|
||||
<ErrorMessage type="not-found" title="Not found">
|
||||
<p>
|
||||
<I18n path="errors.uri_not_found" uri={uri} />
|
||||
</p>
|
||||
</ErrorMessage>
|
||||
);
|
||||
}
|
||||
|
||||
let playlists = playlistsProp;
|
||||
if (sortField) playlists = sortItems(playlists, sortField, sortReverse);
|
||||
if (filter && filter !== '') playlists = applyFilter('name', filter, playlists);
|
||||
|
||||
const sort_options = [
|
||||
{
|
||||
value: null,
|
||||
label: i18n('fields.filters.as_loaded'),
|
||||
},
|
||||
{
|
||||
value: 'name',
|
||||
label: i18n('fields.filters.name'),
|
||||
},
|
||||
{
|
||||
value: 'tracks',
|
||||
label: i18n('fields.filters.tracks'),
|
||||
},
|
||||
];
|
||||
|
||||
const options = (
|
||||
<>
|
||||
<FilterField
|
||||
initialValue={filter}
|
||||
handleChange={setFilter}
|
||||
onSubmit={() => uiActions.hideContextMenu()}
|
||||
/>
|
||||
<DropdownField
|
||||
icon="swap_vert"
|
||||
name={i18n('fields.sort')}
|
||||
value={sortField}
|
||||
valueAsLabel
|
||||
options={sort_options}
|
||||
selected_icon={sortField ? (sortReverse ? 'keyboard_arrow_up' : 'keyboard_arrow_down') : null}
|
||||
handleChange={onSortChange}
|
||||
/>
|
||||
<Button
|
||||
noHover
|
||||
onClick={refresh}
|
||||
tracking={{ category: 'DiscoverCategory', action: 'Refresh' }}
|
||||
>
|
||||
<Icon name="refresh" />
|
||||
<I18n path="actions.refresh" />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="view discover-categories-view">
|
||||
<Header uiActions={uiActions} options={options}>
|
||||
<Icon name="mood" type="material" />
|
||||
{category.name}
|
||||
</Header>
|
||||
<div className="content-wrapper">
|
||||
<section className="grid-wrapper">
|
||||
<Grid items={playlists} />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const [sortField, sortReverse] = getSortSelector(state, SORT_KEY, null);
|
||||
const uri = decodeUri(ownProps.match.params.uri);
|
||||
const loadingSelector = makeLoadingSelector([`spotify_category_${uri}`]);
|
||||
const categorySelector = makeItemSelector(uri);
|
||||
const category = categorySelector(state);
|
||||
let playlists = null;
|
||||
if (category && category.playlists_uris) {
|
||||
const playlistsSelector = makeItemSelector(category.playlists_uris);
|
||||
playlists = playlistsSelector(state);
|
||||
}
|
||||
|
||||
return {
|
||||
uri,
|
||||
loading: loadingSelector(state),
|
||||
playlists,
|
||||
category,
|
||||
sortField,
|
||||
sortReverse,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
coreActions: bindActionCreators(coreActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Category);
|
||||
@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import Recommendations from './Recommendations';
|
||||
import Featured from './Featured';
|
||||
import Categories from './Categories';
|
||||
import Category from './Category';
|
||||
import FeaturedPlaylists from './FeaturedPlaylists';
|
||||
import NewReleases from './NewReleases';
|
||||
import Moods from './Moods';
|
||||
import PlaylistGroup from './PlaylistGroup';
|
||||
import Playlist from '../Playlist';
|
||||
|
||||
export default () => (
|
||||
<Switch>
|
||||
@ -27,18 +26,18 @@ export default () => (
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/discover/featured"
|
||||
component={Featured}
|
||||
path="/discover/featured-playlists"
|
||||
component={FeaturedPlaylists}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/discover/categories/:uri"
|
||||
component={Category}
|
||||
path="/discover/featured-playlists/playlist/:uri/:name?"
|
||||
component={Playlist}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/discover/categories"
|
||||
component={Categories}
|
||||
path="/discover/featured-playlists/playlist_group/:uri/:name?"
|
||||
component={PlaylistGroup}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
|
||||
@ -1,106 +0,0 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { Grid } from '../../components/Grid';
|
||||
import Header from '../../components/Header';
|
||||
import Icon from '../../components/Icon';
|
||||
import Loader from '../../components/Loader';
|
||||
import * as uiActions from '../../services/ui/actions';
|
||||
import * as mopidyActions from '../../services/mopidy/actions';
|
||||
import * as spotifyActions from '../../services/spotify/actions';
|
||||
import { i18n, I18n } from '../../locale';
|
||||
import Button from '../../components/Button';
|
||||
import { indexToArray } from '../../util/arrays';
|
||||
import { makeLoadingSelector } from '../../util/selectors';
|
||||
import { formatSimpleObject } from '../../util/format';
|
||||
|
||||
class Featured extends React.Component {
|
||||
componentDidMount() {
|
||||
const {
|
||||
uris,
|
||||
uiActions: {
|
||||
setWindowTitle,
|
||||
},
|
||||
spotifyActions: {
|
||||
getFeaturedPlaylists,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
setWindowTitle(i18n('discover.featured.title'));
|
||||
if (!uris) getFeaturedPlaylists();
|
||||
}
|
||||
|
||||
onRefresh = () => {
|
||||
const {
|
||||
uiActions: {
|
||||
hideContextMenu,
|
||||
},
|
||||
spotifyActions: {
|
||||
getFeaturedPlaylists,
|
||||
},
|
||||
} = this.props;
|
||||
hideContextMenu();
|
||||
getFeaturedPlaylists(true);
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const {
|
||||
loading,
|
||||
uris,
|
||||
items,
|
||||
} = this.props;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="view discover-featured-view preserve-3d">
|
||||
<Header className="overlay" uiActions={uiActions}>
|
||||
<Icon name="star" type="material" />
|
||||
<I18n path="discover.featured.title" />
|
||||
</Header>
|
||||
<Loader body loading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const playlists = indexToArray(items, uris || []);
|
||||
|
||||
const options = (
|
||||
<Button
|
||||
noHover
|
||||
onClick={this.onRefresh}
|
||||
tracking={{ category: 'DiscoverFeatured', action: 'Refresh' }}
|
||||
>
|
||||
<Icon name="refresh" />
|
||||
<I18n path="actions.refresh" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="view discover-featured-view preserve-3d">
|
||||
<Header uiActions={uiActions} options={options}>
|
||||
<Icon name="star" type="material" />
|
||||
<I18n path="discover.featured.title" />
|
||||
</Header>
|
||||
<section className="content-wrapper grid-wrapper">
|
||||
<Grid items={playlists} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const loadingSelector = makeLoadingSelector(['(.*)featured-playlists(.*)']);
|
||||
const mapStateToProps = (state) => ({
|
||||
theme: state.ui.theme,
|
||||
loading: loadingSelector(state),
|
||||
uris: state.spotify && state.spotify.featured_playlists ? state.spotify.featured_playlists.uris : null,
|
||||
items: state.core.items,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Featured);
|
||||
288
src/js/views/Discover/FeaturedPlaylists.js
Normal file
288
src/js/views/Discover/FeaturedPlaylists.js
Normal file
@ -0,0 +1,288 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import Header from '../../components/Header';
|
||||
import DropdownField from '../../components/Fields/DropdownField';
|
||||
import FilterField from '../../components/Fields/FilterField';
|
||||
import { Grid } from '../../components/Grid';
|
||||
import { List } from '../../components/List';
|
||||
import Icon from '../../components/Icon';
|
||||
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 { sortItems, applyFilter } from '../../util/arrays';
|
||||
import Button from '../../components/Button';
|
||||
import { i18n, I18n } from '../../locale';
|
||||
import Loader from '../../components/Loader';
|
||||
import {
|
||||
makeLibrarySelector,
|
||||
makeProcessProgressSelector,
|
||||
getLibrarySource,
|
||||
makeProvidersSelector,
|
||||
getSortSelector,
|
||||
} from '../../util/selectors';
|
||||
import { encodeUri } from '../../util/format';
|
||||
|
||||
const SORT_KEY = 'library_featured_playlists';
|
||||
const processKeys = [
|
||||
'MOPIDY_GET_LIBRARY_FEATURED_PLAYLISTS',
|
||||
'SPOTIFY_GET_LIBRARY_FEATURED_PLAYLISTS',
|
||||
];
|
||||
|
||||
class FeaturedPlaylists extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filter: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
uiActions: {
|
||||
setWindowTitle,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
setWindowTitle(i18n('discover.featured_playlists.title'));
|
||||
this.getLibraries();
|
||||
}
|
||||
|
||||
componentDidUpdate = ({ source: prevSource }) => {
|
||||
const { source } = this.props;
|
||||
|
||||
if (source !== prevSource) {
|
||||
this.getLibraries();
|
||||
}
|
||||
}
|
||||
|
||||
refresh = () => {
|
||||
const { uiActions: { hideContextMenu } } = this.props;
|
||||
|
||||
hideContextMenu();
|
||||
this.getLibraries(true);
|
||||
}
|
||||
|
||||
cancelRefresh = () => {
|
||||
const { uiActions: { hideContextMenu, cancelProcess } } = this.props;
|
||||
|
||||
hideContextMenu();
|
||||
cancelProcess(processKeys);
|
||||
}
|
||||
|
||||
getLibraries = (forceRefetch = false) => {
|
||||
const {
|
||||
source,
|
||||
providers,
|
||||
coreActions: {
|
||||
loadLibrary,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
let uris = [];
|
||||
if (source === 'all') {
|
||||
uris = providers.map((p) => p.uri);
|
||||
} else {
|
||||
uris.push(source);
|
||||
}
|
||||
uris.forEach((uri) => loadLibrary(uri, 'featuredPlaylists', { forceRefetch }));
|
||||
};
|
||||
|
||||
onSortChange = (field) => {
|
||||
const {
|
||||
sortField,
|
||||
sortReverse,
|
||||
uiActions: {
|
||||
setSort,
|
||||
hideContextMenu,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
let reverse = false;
|
||||
if (field !== null && sortField === field) {
|
||||
reverse = !sortReverse;
|
||||
}
|
||||
|
||||
setSort(SORT_KEY, field, reverse);
|
||||
hideContextMenu();
|
||||
}
|
||||
|
||||
renderView = () => {
|
||||
const {
|
||||
sortField,
|
||||
sortReverse,
|
||||
view,
|
||||
loading_progress,
|
||||
} = this.props;
|
||||
const {
|
||||
filter,
|
||||
} = this.state;
|
||||
let { featured_playlists } = this.props;
|
||||
|
||||
if (loading_progress) {
|
||||
return <Loader body loading progress={loading_progress} />;
|
||||
}
|
||||
|
||||
if (sortField) {
|
||||
featured_playlists = sortItems(featured_playlists, sortField, sortReverse);
|
||||
}
|
||||
|
||||
if (filter && filter !== '') {
|
||||
featured_playlists = applyFilter('name', filter, featured_playlists);
|
||||
}
|
||||
|
||||
if (view === 'list') {
|
||||
return (
|
||||
<section className="content-wrapper">
|
||||
<List
|
||||
items={featured_playlists}
|
||||
details={['playlists']}
|
||||
right_column={['source']}
|
||||
getLink={(item) => `/discover/featured-playlists/${item.type}/${encodeUri(item.uri)}/${item.name}`}
|
||||
thumbnail
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<section className="content-wrapper">
|
||||
<Grid
|
||||
items={featured_playlists}
|
||||
getLink={(item) => `/discover/featured-playlists/${item.type}/${encodeUri(item.uri)}/${item.name}`}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const {
|
||||
view,
|
||||
source,
|
||||
providers,
|
||||
sortField,
|
||||
sortReverse,
|
||||
uiActions,
|
||||
loading_progress,
|
||||
} = this.props;
|
||||
const {
|
||||
filter,
|
||||
per_page,
|
||||
} = this.state;
|
||||
|
||||
const view_options = [
|
||||
{
|
||||
value: 'thumbnails',
|
||||
label: i18n('fields.filters.thumbnails'),
|
||||
},
|
||||
{
|
||||
value: 'list',
|
||||
label: i18n('fields.filters.list'),
|
||||
},
|
||||
];
|
||||
|
||||
const sort_options = [
|
||||
{
|
||||
value: null,
|
||||
label: i18n('fields.filters.as_loaded'),
|
||||
},
|
||||
{
|
||||
value: 'name',
|
||||
label: i18n('fields.filters.name'),
|
||||
},
|
||||
{
|
||||
value: 'uri',
|
||||
label: i18n('fields.filters.source'),
|
||||
},
|
||||
];
|
||||
|
||||
console.debug({ source, view })
|
||||
|
||||
const options = (
|
||||
<>
|
||||
<FilterField
|
||||
initialValue={filter}
|
||||
handleChange={(value) => this.setState({ filter: value, limit: per_page })}
|
||||
onSubmit={() => uiActions.hideContextMenu()}
|
||||
/>
|
||||
<DropdownField
|
||||
icon="swap_vert"
|
||||
name={i18n('fields.sort')}
|
||||
value={sortField}
|
||||
valueAsLabel
|
||||
options={sort_options}
|
||||
selected_icon={sortField ? (sortReverse ? 'keyboard_arrow_up' : 'keyboard_arrow_down') : null}
|
||||
handleChange={this.onSortChange}
|
||||
/>
|
||||
<DropdownField
|
||||
icon="visibility"
|
||||
name={i18n('fields.view')}
|
||||
value={view}
|
||||
valueAsLabel
|
||||
options={view_options}
|
||||
handleChange={(val) => { uiActions.set({ library_featured_playlists_view: val }); uiActions.hideContextMenu(); }}
|
||||
/>
|
||||
<DropdownField
|
||||
icon="cloud"
|
||||
name={i18n('fields.source')}
|
||||
value={source}
|
||||
valueAsLabel
|
||||
options={[
|
||||
{
|
||||
value: 'all',
|
||||
label: i18n('fields.filters.all'),
|
||||
},
|
||||
...providers.map((p) => ({ value: p.uri, label: p.title })),
|
||||
]}
|
||||
handleChange={(val) => { uiActions.set({ library_featured_playlists_source: val }); uiActions.hideContextMenu(); }}
|
||||
/>
|
||||
<Button
|
||||
noHover
|
||||
discrete
|
||||
onClick={loading_progress ? this.cancelRefresh : this.refresh}
|
||||
tracking={{ category: 'FeaturedPlaylists', action: 'Refresh' }}
|
||||
>
|
||||
{loading_progress ? <Icon name="close" /> : <Icon name="refresh" /> }
|
||||
{loading_progress ? <I18n path="actions.cancel" /> : <I18n path="actions.refresh" /> }
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="view library-featured-playlists-view">
|
||||
<Header options={options} uiActions={uiActions}>
|
||||
<Icon name="album" type="material" />
|
||||
<I18n path="discover.featured_playlists.title" />
|
||||
</Header>
|
||||
{this.renderView()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const librarySelector = makeLibrarySelector('featured_playlists');
|
||||
const processProgressSelector = makeProcessProgressSelector(processKeys);
|
||||
const providersSelector = makeProvidersSelector('featured_playlists');
|
||||
const mapStateToProps = (state) => {
|
||||
const [sortField, sortReverse] = getSortSelector(state, SORT_KEY, null);
|
||||
|
||||
return {
|
||||
loading_progress: processProgressSelector(state),
|
||||
uri_schemes: state.mopidy.uri_schemes,
|
||||
featured_playlists: librarySelector(state, 'featured_playlists'),
|
||||
providers: providersSelector(state),
|
||||
view: state.ui.library_featured_playlists_view,
|
||||
source: getLibrarySource(state, 'featured_playlists'),
|
||||
sortField,
|
||||
sortReverse,
|
||||
};
|
||||
};
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
coreActions: bindActionCreators(coreActions, dispatch),
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FeaturedPlaylists);
|
||||
@ -18,7 +18,8 @@
|
||||
&__thumbnail {
|
||||
@include animate();
|
||||
max-width: 100%;
|
||||
|
||||
z-index: unset;
|
||||
|
||||
&__image {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
@ -38,38 +39,12 @@
|
||||
padding-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
.thumbnail {
|
||||
&__image {
|
||||
transform: scale(1.01);
|
||||
|
||||
&--glow {
|
||||
/*@include blur(20px);*/
|
||||
transform: rotateX(6deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
.thumbnail {
|
||||
&__image {
|
||||
@include noanimate();
|
||||
-moz-transform: scale(0.98);
|
||||
-webkit-transform: scale(0.98);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--tile {
|
||||
.grid__item {
|
||||
&--tile {
|
||||
position: relative;
|
||||
|
||||
&__name {
|
||||
|
||||
.grid__item__name {
|
||||
position: absolute;
|
||||
bottom: 15%;
|
||||
left: 0;
|
||||
@ -95,9 +70,32 @@
|
||||
}
|
||||
|
||||
@include responsive(null, $bp_wide) {
|
||||
.grid__item {
|
||||
&__name {
|
||||
font-size: 16px;
|
||||
.grid__item__name {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.thumbnail {
|
||||
&__image {
|
||||
transform: scale(1.01);
|
||||
|
||||
&--glow {
|
||||
/*@include blur(20px);*/
|
||||
transform: rotateX(6deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
.thumbnail {
|
||||
&__image {
|
||||
@include noanimate();
|
||||
-moz-transform: scale(0.98);
|
||||
-webkit-transform: scale(0.98);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user