From 0118abb6ec77b21742c7bdfa95ea293c2da2ff8a Mon Sep 17 00:00:00 2001 From: jeffvli Date: Thu, 4 Nov 2021 16:56:44 -0700 Subject: [PATCH] Add dynamic gradient background for artist page --- package.json | 1 + src/components/layout/GenericPage.tsx | 2 +- src/components/layout/styled.tsx | 44 ++- src/components/library/ArtistView.tsx | 419 +++++++++++++++----------- yarn.lock | 23 +- 5 files changed, 294 insertions(+), 195 deletions(-) diff --git a/package.json b/package.json index 00c43c3..beec62b 100644 --- a/package.json +++ b/package.json @@ -260,6 +260,7 @@ "electron-redux": "^1.5.4", "electron-settings": "^4.0.2", "electron-updater": "^4.3.4", + "fast-average-color": "^7.0.1", "format-duration": "^1.4.0", "history": "^5.0.0", "image-downloader": "^4.0.3", diff --git a/src/components/layout/GenericPage.tsx b/src/components/layout/GenericPage.tsx index 28b4734..d0503a4 100644 --- a/src/components/layout/GenericPage.tsx +++ b/src/components/layout/GenericPage.tsx @@ -48,7 +48,7 @@ const GenericPage = ({ header, children, hideDivider, ...rest }: any) => { {header} {!hideDivider && } - + {children} diff --git a/src/components/layout/styled.tsx b/src/components/layout/styled.tsx index 0b44823..acbec37 100644 --- a/src/components/layout/styled.tsx +++ b/src/components/layout/styled.tsx @@ -153,10 +153,10 @@ export const PageHeader = styled(Header)<{ padding?: string }>` z-index: 1; `; -export const PageContent = styled(Content)<{ padding?: string }>` +export const PageContent = styled(Content)<{ padding?: string; $zIndex?: number }>` position: relative; padding: ${(props) => (props.padding ? props.padding : '10px')}; - z-index: 1; + z-index: ${(props) => props.$zIndex}; `; // Sidebar.tsx @@ -213,12 +213,16 @@ export const PageHeaderTitle = styled.h1` } `; -export const PageHeaderWrapper = styled.div<{ $hasImage: boolean; $imageHeight: string }>` - display: ${(props) => (props.$hasImage ? 'inline-block' : 'undefined')}; - width: ${(props) => (props.$hasImage ? `calc(100% - ${props.$imageHeight + 15}px)` : '100%')}; - margin-left: ${(props) => (props.$hasImage ? '15px' : '0px')}; +export const PageHeaderWrapper = styled.div<{ + hasImage: boolean; + imageHeight: string; + isDark?: boolean; +}>` + display: ${(props) => (props.hasImage ? 'inline-block' : 'undefined')}; + width: ${(props) => (props.hasImage ? `calc(100% - ${props.imageHeight + 15}px)` : '100%')}; + margin-left: ${(props) => (props.hasImage ? '15px' : '0px')}; vertical-align: top; - color: ${(props) => (props.$hasImage ? '#D8D8D8' : props.theme.colors.layout.page.color)}; + color: ${(props) => (props.isDark ? '#D8D8D8' : props.theme.colors.layout.page.color)}; `; export const PageHeaderSubtitleWrapper = styled.span` @@ -228,7 +232,15 @@ export const PageHeaderSubtitleWrapper = styled.span` `; export const PageHeaderSubtitleDataLine = styled.div<{ $top?: boolean }>` - margin-top: ${(props) => (props.$top ? '0px' : '10px')}; + margin-top: ${(props) => (props.$top ? '0px' : '7px')}; + white-space: nowrap; + overflow: visible; + + ::-webkit-scrollbar { + height: 0px; + } + + scroll-behavior: smooth; `; export const FlatBackground = styled.div<{ $expanded: boolean; $color: string }>` @@ -238,29 +250,29 @@ export const FlatBackground = styled.div<{ $expanded: boolean; $color: string }> height: 200px; position: absolute; width: ${(props) => (props.$expanded ? `calc(100% - 165px)` : 'calc(100% - 56px)')}; - z-index: 1; user-select: none; pointer-events: none; `; -export const BlurredBackgroundWrapper = styled.div<{ $expanded: boolean }>` +export const BlurredBackgroundWrapper = styled.div<{ expanded: boolean; image: string }>` clip: rect(0, auto, auto, 0); -webkit-clip-path: inset(0 0); clip-path: inset(0 0); position: absolute; - left: ${(props) => (props.$expanded ? '165px' : '56px')}; - width: ${(props) => (props.$expanded ? `calc(100% - 165px)` : 'calc(100% - 56px)')}; + left: ${(props) => (props.expanded ? '165px' : '56px')}; + width: ${(props) => (props.expanded ? `calc(100% - 165px)` : 'calc(100% - 56px)')}; top: 32px; z-index: 1; display: block; - background: #0b0908; + background: ${(props) => (props.image ? '#0b0908' : '#00395A')}; + filter: ${(props) => (props.image ? 'none' : 'brightness(0.3)')}; `; -export const BlurredBackground = styled.img<{ $expanded: boolean; $image: string }>` - background-image: ${(props) => `url(${props.$image})`}; +export const BlurredBackground = styled.img<{ expanded: boolean; image: string }>` + background-image: ${(props) => (props.image ? `url(${props.image})` : 'none')}; background-position: center 30%; background-size: cover; - filter: blur(10px) brightness(0.4); + filter: blur(10px) brightness(0.3); outline: none !important; border: none !important; diff --git a/src/components/library/ArtistView.tsx b/src/components/library/ArtistView.tsx index c27a45d..f7d4975 100644 --- a/src/components/library/ArtistView.tsx +++ b/src/components/library/ArtistView.tsx @@ -1,6 +1,7 @@ /* eslint-disable import/no-cycle */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import _ from 'lodash'; +import FastAverageColor from 'fast-average-color'; import { shell } from 'electron'; import settings from 'electron-settings'; import { ButtonToolbar, Whisper, TagGroup } from 'rsuite'; @@ -35,9 +36,17 @@ import { setPlayQueue, } from '../../redux/playQueueSlice'; import { notifyToast } from '../shared/toast'; -import { filterPlayQueue, getPlayedSongsNotification, isCached } from '../../shared/utils'; +import { + filterPlayQueue, + formatDuration, + getPlayedSongsNotification, + isCached, +} from '../../shared/utils'; import { StyledButton, StyledPopover, StyledTag } from '../shared/styled'; import { setStatus } from '../../redux/playerSlice'; +import { GradientBackground, PageHeaderSubtitleDataLine } from '../layout/styled'; + +const fac = new FastAverageColor(); interface ArtistParams { id: string; @@ -50,6 +59,10 @@ const ArtistView = ({ ...rest }: any) => { const misc = useAppSelector((state) => state.misc); const config = useAppSelector((state) => state.config); const [viewType, setViewType] = useState(settings.getSync('albumViewType') || 'list'); + const [imageAverageColor, setImageAverageColor] = useState({ color: '', loaded: false }); + const [artistDurationTotal, setArtistDurationTotal] = useState(''); + const [artistSongTotal, setArtistSongTotal] = useState(0); + const { id } = useParams(); const artistId = rest.id ? rest.id : id; const { isLoading, isError, data, error }: any = useQuery(['artist', artistId], () => @@ -131,18 +144,6 @@ const ArtistView = ({ ...rest }: any) => { notifyToast('info', getPlayedSongsNotification({ ...songs.count, type: 'add' })); }; - if (isLoading || isLoadingAI) { - return ; - } - - if (isError || isErrorAI) { - return ( - - Error: {error?.message} {errorAI?.message} - - ); - } - const handleRowFavorite = async (rowData: any) => { if (!rowData.starred) { await star(rowData.id, 'album'); @@ -167,168 +168,248 @@ const ArtistView = ({ ...rest }: any) => { } }; + useEffect(() => { + if (!isLoading) { + const img = isCached(`${misc.imageCachePath}artist_${data?.id}.jpg`) + ? `${misc.imageCachePath}artist_${data?.id}.jpg` + : data?.image.includes('placeholder') + ? artistInfo?.largeImageUrl && + !artistInfo?.largeImageUrl?.match('2a96cbd8b46e442fc41c2b86b821562f') + ? artistInfo?.largeImageUrl + : data?.image + : data?.image; + + const setAvgColor = (imgUrl: string) => { + if ( + data?.image.match('placeholder') || + (data?.image.match('placeholder') && + artistInfo?.largeImageUrl?.match('2a96cbd8b46e442fc41c2b86b821562f')) + ) { + setImageAverageColor({ color: 'rgba(0, 57, 90, .4)', loaded: true }); + } else { + fac + .getColorAsync(imgUrl, { + ignoredColor: [ + [255, 255, 255, 255], // White + [0, 0, 0, 255], // Black + ], + mode: 'precision', + algorithm: 'dominant', + }) + .then((color) => { + return setImageAverageColor({ + color: color.rgba.replace(',1)', ',0.4)'), + loaded: true, + }); + }) + .catch(() => setAvgColor(imgUrl)); + } + }; + setAvgColor(img); + } + }, [artistInfo?.largeImageUrl, data?.id, data?.image, isLoading, misc.imageCachePath]); + + useEffect(() => { + const allAlbumDurations = _.sum(_.map(data?.album, 'duration')); + const allSongCount = _.sum(_.map(data?.album, 'songCount')); + + setArtistDurationTotal(formatDuration(allAlbumDurations) || 'N/a'); + setArtistSongTotal(allSongCount); + }, [data?.album]); + + if (isLoading || isLoadingAI || imageAverageColor.loaded === false) { + return ; + } + + if (isError || isErrorAI) { + return ( + + Error: {error?.message} {errorAI?.message} + + ); + } + return ( - + + - ]*>/, '') - .replace('Read more on Last.fm', '')} - placement="bottomStart" - > - - {artistInfo?.biography - ?.replace(/<[^>]*>/, '') - .replace('Read more on Last.fm', '') - ?.trim() - ? `${artistInfo?.biography - ?.replace(/<[^>]*>/, '') - .replace('Read more on Last.fm', '')}` - : 'No artist biography found'} - - -
- - - handlePlayAppend('next')} - /> - handlePlayAppend('later')} - /> - - -
-
Related artists
- - {artistInfo.similarArtist?.map((artist: any) => ( - { - if (!rest.isModal) { - history.push(`/library/artist/${artist.id}`); - } else { - dispatch( - addModalPage({ - pageType: 'artist', - id: artist.id, - }) - ); - } - }} - > - {artist.name} - - ))} - -
-
- shell.openExternal(artistInfo?.lastFmUrl)} - > - View on Last.FM - - - } - > - Info -
-
-
- - } - searchQuery={searchQuery} - handleSearch={(e: any) => setSearchQuery(e)} - clearSearchQuery={() => setSearchQuery('')} - showSearchBar - showViewTypeButtons - viewTypeSetting="album" - handleListClick={() => setViewType('list')} - handleGridClick={() => setViewType('grid')} - /> - } - > - <> - {viewType === 'list' && ( - - )} + imageHeight={185} + title={data.name} + showTitleTooltip + subtitle={ + <> + + ARTIST • {data.albumCount} albums • {artistSongTotal} songs •{' '} + {artistDurationTotal} + + + ]*>/, '') + .replace('Read more on Last.fm', '')} + placement="bottomStart" + > + + {artistInfo?.biography + ?.replace(/<[^>]*>/, '') + .replace('Read more on Last.fm', '') + ?.trim() + ? `${artistInfo?.biography + ?.replace(/<[^>]*>/, '') + .replace('Read more on Last.fm', '')}` + : 'No artist biography found'} + + + - {viewType === 'grid' && ( - + + + handlePlayAppend('next')} + /> + handlePlayAppend('later')} + /> + + +
+
Related artists
+ + {artistInfo.similarArtist?.map((artist: any) => ( + { + if (!rest.isModal) { + history.push(`/library/artist/${artist.id}`); + } else { + dispatch( + addModalPage({ + pageType: 'artist', + id: artist.id, + }) + ); + } + }} + > + {artist.name} + + ))} + +
+
+ shell.openExternal(artistInfo?.lastFmUrl)} + > + View on Last.FM + + + } + > + Info +
+
+ + + } + searchQuery={searchQuery} + handleSearch={(e: any) => setSearchQuery(e)} + clearSearchQuery={() => setSearchQuery('')} + showSearchBar + showViewTypeButtons + viewTypeSetting="album" + handleListClick={() => setViewType('list')} + handleGridClick={() => setViewType('grid')} /> - )} - -
+ } + > + <> + {viewType === 'list' && ( + + )} + + {viewType === 'grid' && ( + + )} + +
+ ); }; diff --git a/yarn.lock b/yarn.lock index acc1f61..47c13ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1375,14 +1375,7 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@malept/cross-spawn-promise@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.0.tgz#258fde4098f5004a56db67c35f33033af64810f6" - integrity sha512-GeIK5rfU1Yd7BZJQPTGZMMmcZy5nhRToPXZcjaDwQDRSewdhp648GT2E4dh+L7+Io7AOW6WQ+GR44QSzja4qxg== - dependencies: - cross-spawn "^7.0.1" - -"@malept/cross-spawn-promise@^1.1.1": +"@malept/cross-spawn-promise@^1.1.0", "@malept/cross-spawn-promise@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz#504af200af6b98e198bce768bc1730c6936ae01d" integrity sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ== @@ -1766,6 +1759,11 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/offscreencanvas@^2019.6.4": + version "2019.6.4" + resolved "https://registry.yarnpkg.com/@types/offscreencanvas/-/offscreencanvas-2019.6.4.tgz#64f6d120b53925028299c744fcdd32d2cd525963" + integrity sha512-u8SAgdZ8ROtkTF+mfZGOscl0or6BSj9A4g37e6nvxDc+YB/oDut0wHkK2PBBiC2bNR8TS0CPV+1gAk4fNisr1Q== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -5722,6 +5720,13 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +fast-average-color@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fast-average-color/-/fast-average-color-7.0.1.tgz#2304cc62578f4427c1bc48844faa1c5178835b67" + integrity sha512-M9Lg7mO/sSGezi7cB/i+I9ym33IBcdeREHk222//3sbtj2+Aq9Cv3DIVY1asn3fu4rX2ENQ2iv2mCSaVpC68SQ== + dependencies: + "@types/offscreencanvas" "^2019.6.4" + fast-deep-equal@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" @@ -11396,7 +11401,7 @@ source-map-resolve@^0.6.0: atob "^2.1.2" decode-uri-component "^0.2.0" -source-map-support@^0.5.11, source-map-support@^0.5.16, source-map-support@^0.5.19, source-map-support@^0.5.6, source-map-support@~0.5.19: +source-map-support@^0.5.16, source-map-support@^0.5.19, source-map-support@^0.5.6, source-map-support@~0.5.19: version "0.5.20" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==