From 238d63a6ecd12eb4e4e2891002162ff517fe2e9f Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Sat, 18 Jun 2022 16:49:12 +1200 Subject: [PATCH] Refactoring Featured Playlists for multi-source support --- src/js/components/Grid.js | 4 +- src/js/components/GridItem.js | 3 +- src/js/components/Sidebar.js | 2 +- src/js/locale/en.yaml | 3 +- src/js/services/core/middleware.js | 1 + src/js/services/mopidy/actions.js | 7 + src/js/services/mopidy/middleware.js | 112 ++++++-- src/js/services/spotify/actions.js | 13 +- src/js/util/selectors.js | 12 + src/js/views/Discover/Categories.js | 102 -------- src/js/views/Discover/Category.js | 173 ------------- src/js/views/Discover/Discover.js | 17 +- src/js/views/Discover/Featured.js | 106 -------- src/js/views/Discover/FeaturedPlaylists.js | 288 +++++++++++++++++++++ src/scss/components/_grid.scss | 66 +++-- 15 files changed, 454 insertions(+), 455 deletions(-) delete mode 100755 src/js/views/Discover/Categories.js delete mode 100755 src/js/views/Discover/Category.js delete mode 100755 src/js/views/Discover/Featured.js create mode 100644 src/js/views/Discover/FeaturedPlaylists.js diff --git a/src/js/components/Grid.js b/src/js/components/Grid.js index 9c2d3d27..7c42e99a 100644 --- a/src/js/components/Grid.js +++ b/src/js/components/Grid.js @@ -6,7 +6,6 @@ const Grid = memo(({ items, className = '', mini, - tile, getLink, sourceIcon, }) => { @@ -14,13 +13,12 @@ const Grid = memo(({ return ( ); diff --git a/src/js/components/GridItem.js b/src/js/components/GridItem.js index 39dad86b..08a7e799 100755 --- a/src/js/components/GridItem.js +++ b/src/js/components/GridItem.js @@ -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 (
{ {spotify_available && ( - + diff --git a/src/js/locale/en.yaml b/src/js/locale/en.yaml index fee63b96..8bf96a22 100755 --- a/src/js/locale/en.yaml +++ b/src/js/locale/en.yaml @@ -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 diff --git a/src/js/services/core/middleware.js b/src/js/services/core/middleware.js index 19d6740a..be1a4b6b 100755 --- a/src/js/services/core/middleware.js +++ b/src/js/services/core/middleware.js @@ -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': diff --git a/src/js/services/mopidy/actions.js b/src/js/services/mopidy/actions.js index da13c5e5..e13fcae0 100755 --- a/src/js/services/mopidy/actions.js +++ b/src/js/services/mopidy/actions.js @@ -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', diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index 766fe03d..98b2ca12 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -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; } diff --git a/src/js/services/spotify/actions.js b/src/js/services/spotify/actions.js index d662fa6d..317fe346 100755 --- a/src/js/services/spotify/actions.js +++ b/src/js/services/spotify/actions.js @@ -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)), diff --git a/src/js/util/selectors.js b/src/js/util/selectors.js index b884a463..d4bdbf9b 100755 --- a/src/js/util/selectors.js +++ b/src/js/util/selectors.js @@ -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 || []; diff --git a/src/js/views/Discover/Categories.js b/src/js/views/Discover/Categories.js deleted file mode 100755 index 9101c21b..00000000 --- a/src/js/views/Discover/Categories.js +++ /dev/null @@ -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 = ( - <> - hideContextMenu()} - /> - - - ); - - return ( -
-
- - -
-
- {loading ? ( - - ) : ( - `/discover/categories/${encodeUri(item.uri)}`} - sourceIcon={false} - /> - )} -
-
- ); -} - -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); diff --git a/src/js/views/Discover/Category.js b/src/js/views/Discover/Category.js deleted file mode 100755 index 09017738..00000000 --- a/src/js/views/Discover/Category.js +++ /dev/null @@ -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 ; - } - - if (!category) { - return ( - -

- -

-
- ); - } - - 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 = ( - <> - uiActions.hideContextMenu()} - /> - - - - ); - - return ( -
-
- - {category.name} -
-
-
- -
-
-
- ); -} - -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); diff --git a/src/js/views/Discover/Discover.js b/src/js/views/Discover/Discover.js index 06fe47f0..6df8d7eb 100644 --- a/src/js/views/Discover/Discover.js +++ b/src/js/views/Discover/Discover.js @@ -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 () => ( @@ -27,18 +26,18 @@ export default () => ( /> { - const { - uiActions: { - hideContextMenu, - }, - spotifyActions: { - getFeaturedPlaylists, - }, - } = this.props; - hideContextMenu(); - getFeaturedPlaylists(true); - } - - render = () => { - const { - loading, - uris, - items, - } = this.props; - - if (loading) { - return ( -
-
- - -
- -
- ); - } - - const playlists = indexToArray(items, uris || []); - - const options = ( - - ); - - return ( -
-
- - -
-
- -
-
- ); - } -} - -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); diff --git a/src/js/views/Discover/FeaturedPlaylists.js b/src/js/views/Discover/FeaturedPlaylists.js new file mode 100644 index 00000000..6f9f15d4 --- /dev/null +++ b/src/js/views/Discover/FeaturedPlaylists.js @@ -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 ; + } + + if (sortField) { + featured_playlists = sortItems(featured_playlists, sortField, sortReverse); + } + + if (filter && filter !== '') { + featured_playlists = applyFilter('name', filter, featured_playlists); + } + + if (view === 'list') { + return ( +
+ `/discover/featured-playlists/${item.type}/${encodeUri(item.uri)}/${item.name}`} + thumbnail + /> +
+ ); + } + return ( +
+ `/discover/featured-playlists/${item.type}/${encodeUri(item.uri)}/${item.name}`} + /> +
+ ); + } + + 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 = ( + <> + this.setState({ filter: value, limit: per_page })} + onSubmit={() => uiActions.hideContextMenu()} + /> + + { uiActions.set({ library_featured_playlists_view: val }); uiActions.hideContextMenu(); }} + /> + ({ value: p.uri, label: p.title })), + ]} + handleChange={(val) => { uiActions.set({ library_featured_playlists_source: val }); uiActions.hideContextMenu(); }} + /> + + + ); + + return ( +
+
+ + +
+ {this.renderView()} +
+ ); + } +} + +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); diff --git a/src/scss/components/_grid.scss b/src/scss/components/_grid.scss index 2dee6eb1..899c3092 100755 --- a/src/scss/components/_grid.scss +++ b/src/scss/components/_grid.scss @@ -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); } } }