11import { memo , FC , useEffect , useState , useCallback } from 'react' ;
2- import { prominent } from 'color.js' ;
3-
4- import { rgbToHsl , useVisibilityChange } from 'services/utils' ;
5- import { useSiteContext } from 'services/site/context' ;
6- import { TNowPlayingData } from 'services/now-playing' ;
7- import { fetchNowPlaying } from 'services/now-playing/fetch' ;
82import TextLoop from 'react-text-loop' ;
3+ import { styled } from 'goober' ;
94
10- const getBestTextColor = async ( coverArt : string ) => {
11- const colors = ( await prominent ( coverArt , {
12- amount : 3 ,
13- group : 20 ,
14- format : 'array' ,
15- sample : 10 ,
16- } ) ) as number [ ] [ ] ;
17-
18- let [ bestH , bestS , bestL ] = rgbToHsl ( colors [ 0 ] ) ;
19- for ( const rgb of colors ) {
20- const [ h , s , l ] = rgbToHsl ( rgb ) ;
21- if ( s > 40 ) {
22- [ bestH , bestS , bestL ] = [ h , s , l ] ;
23- break ;
24- }
5+ import { useVisibilityChange } from 'services/utils' ;
6+ import { useSiteContext } from 'services/site/context' ;
7+ import {
8+ TNowPlayingData ,
9+ isNowPlayingData ,
10+ getNowPlaying ,
11+ } from 'services/now-playing' ;
12+
13+ const CoverArtLink = styled ( 'a' ) `
14+ position: relative;
15+ display: inline-block ;
16+
17+ transition: transform 250ms ;
18+ &:hover {
19+ transform: scale(1.1);
2520 }
2621
27- // upper bound lightness value at 40 to make it readable
28- return `hsl(${ bestH } , ${ bestS } %, ${ Math . min ( bestL , 40 ) } %)` ;
29- } ;
30-
31- const CoverArtLink = ( { href, children } ) => {
32- const [ hover , setHover ] = useState ( false ) ;
33-
34- return (
35- < a
36- href = { href }
37- target = "_blank"
38- rel = "noreferrer noopener"
39- onMouseEnter = { ( ) => setHover ( true ) }
40- onMouseLeave = { ( ) => setHover ( false ) }
41- style = { {
42- position : 'relative' ,
43- display : 'inline-block' ,
44- transform : `scale(${ hover ? 1.1 : 1 } )` ,
45- transition : 'transform 250ms' ,
46- } }
47- >
48- { children }
49- </ a >
50- ) ;
51- } ;
22+ & img {
23+ width: 18px;
24+ height: 18px;
25+ border-radius: 3px;
26+ transform: translateY(1px);
27+ box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
28+ }
29+ ` ;
5230
5331const nowPlayingMarkup = ( {
5432 name,
55- artist,
33+ artistName,
34+ podcastName,
5635 link,
5736 coverArtSrc,
5837 coverArtColor,
5938} : TNowPlayingData ) => {
60- const isTrack = ! ! artist ;
61- const action = isTrack ? "jammin' out to" : 'listening to' ;
62- const label = `${ name } ${ isTrack ? ` by ${ artist } ` : '' } ` ;
39+ const isPodcast = ! ! podcastName ;
40+ const hasArtist = ! ! artistName ;
41+ const action = isPodcast ? 'listening to an episode of' : "jammin' out to" ;
42+ const label = `${ isPodcast ? podcastName : name } ${
43+ hasArtist ? ` by ${ artistName } ` : ''
44+ } `;
6345
6446 return [
65- ...action . split ( ' ' ) . map ( ( a ) => < span style = { { color : '#000' } } > { a } </ span > ) ,
47+ ...action . split ( ' ' ) . map ( ( a ) => < span > { a } </ span > ) ,
6648 ...label . split ( ' ' ) . map ( ( a ) => (
6749 < span
6850 className = "dynamic"
@@ -71,85 +53,71 @@ const nowPlayingMarkup = ({
7153 { a }
7254 </ span >
7355 ) ) ,
74- < CoverArtLink href = { link } >
75- < img
76- src = { coverArtSrc }
77- style = { {
78- height : '18px' ,
79- borderRadius : '3px' ,
80- transform : 'translateY(1px)' ,
81- } }
82- />
56+ < CoverArtLink href = { link } target = "_blank" rel = "noreferrer noopener" >
57+ < img src = { coverArtSrc } />
8358 </ CoverArtLink > ,
8459 ] ;
8560} ;
8661
8762const DynamicCurrentStatus : FC = memo ( ( ) => {
88- const { nowPlaying , activity, spotifyToken } = useSiteContext ( ) ;
89- const [ np , setNp ] = useState < ( TNowPlayingData | string ) [ ] > ( [
90- nowPlaying ?? `probably ${ activity } ` ,
63+ const { nowPlayingData , activity, spotifyToken } = useSiteContext ( ) ;
64+ const [ statuses , setStatuses ] = useState < ( TNowPlayingData | string ) [ ] > ( [
65+ nowPlayingData ?? `probably ${ activity } ` ,
9166 ] ) ;
92- const [ shouldFetchNew , setShouldFetchNew ] = useState ( true ) ;
9367
9468 const refetchNp = useCallback ( async ( ) => {
95- const lastNp = np [ np . length - 1 ] ;
96- const lastNowPlayingData = typeof lastNp === 'string' ? null : lastNp ;
97- const updatedNowPlayingData = await fetchNowPlaying ( spotifyToken ) ;
98- console . debug ( updatedNowPlayingData , lastNowPlayingData ) ;
69+ const updatedNowPlayingData = await getNowPlaying ( spotifyToken ) ;
9970
100- if (
101- updatedNowPlayingData &&
102- updatedNowPlayingData . uri !== lastNowPlayingData ?. uri
103- ) {
71+ const lastStatus = statuses [ statuses . length - 1 ] ;
72+ const lastNowPlayingData = isNowPlayingData ( lastStatus ) ? lastStatus : null ;
73+ const hasNewNowPlayingData =
74+ ! ! updatedNowPlayingData &&
75+ updatedNowPlayingData . uri !== lastNowPlayingData ?. uri ;
76+
77+ if ( hasNewNowPlayingData ) {
10478 console . debug ( 'New now playing data found...' , updatedNowPlayingData ) ;
105- const color = await getBestTextColor ( updatedNowPlayingData . coverArtSrc ) ;
106- setNp ( ( prev ) => [
107- ...prev ,
108- {
109- ...updatedNowPlayingData ,
110- coverArtColor : color ,
111- } ,
112- ] ) ;
79+ setStatuses ( ( prev ) => [ ...prev , updatedNowPlayingData ] ) ;
11380 }
114- } , [ np , spotifyToken ] ) ;
81+ } , [ statuses , spotifyToken ] ) ;
11582
83+ /**
84+ *Refetch what's currently playing on Spotify when tab receives focus, and on mount.
85+ */
11686 useVisibilityChange ( ( isHidden ) => {
11787 if ( ! isHidden ) {
11888 console . debug ( 'Received focus, refreshing now playing...' ) ;
119- setShouldFetchNew ( true ) ;
89+ refetchNp ( ) ;
12090 }
12191 } ) ;
12292
12393 useEffect ( ( ) => {
124- ( async ( ) => {
125- if ( shouldFetchNew ) {
126- await refetchNp ( ) ;
127- setShouldFetchNew ( false ) ;
128- }
129- } ) ( ) ;
130- } , [ refetchNp , shouldFetchNew ] ) ;
94+ refetchNp ( ) ;
95+ // eslint-disable-next-line react-hooks/exhaustive-deps
96+ } , [ ] ) ;
13197
132- const npMarkup = np . map ( ( e ) => {
133- return typeof e === 'string' ? e . split ( ' ' ) : nowPlayingMarkup ( e ) ;
134- } ) ;
98+ const statusesMarkup = statuses . map ( ( status ) =>
99+ isNowPlayingData ( status ) ? nowPlayingMarkup ( status ) : status . split ( ' ' )
100+ ) ;
135101
136102 return (
137103 < span >
138- { [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 ] . map (
139- ( i ) => {
104+ { new Array ( Math . max ( ...statusesMarkup . map ( ( s ) => s . length ) ) )
105+ . fill ( '' )
106+ . map ( ( _ , wordIdx ) => {
140107 return (
141108 < >
142109 < TextLoop
143- interval = { np . map ( ( _ , i ) => ( i === np . length - 1 ? - 1 : 2000 ) ) } // don't transition from the last data back to the initial data
144- children = { npMarkup . map ( ( e ) => e [ i ] ?? ' ' ) }
110+ // transition to next status, but don't transition from last back to first
111+ interval = { statuses . map ( ( _ , i ) =>
112+ i === statuses . length - 1 ? - 1 : 1000
113+ ) }
114+ children = { statusesMarkup . map ( ( m ) => m [ wordIdx ] ?? '' ) }
145115 /> { ' ' }
146116 </ >
147117 ) ;
148- }
149- ) }
118+ } ) }
150119 </ span >
151120 ) ;
152121} ) ;
153- // TODO: remove react-motion
154122
155123export default DynamicCurrentStatus ;
0 commit comments