Refactoring Featured Playlists for multi-source support

This commit is contained in:
James Barnsley
2022-06-18 16:49:12 +12:00
parent b84c7c3b75
commit 238d63a6ec
15 changed files with 454 additions and 455 deletions

View File

@ -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,
}}
/>
);

View File

@ -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}

View File

@ -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>

View File

@ -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

View File

@ -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':

View File

@ -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',

View File

@ -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;
}

View File

@ -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)),

View File

@ -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 || [];

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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);

View 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);

View File

@ -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);
}
}
}