11import { Avatar , AvatarFallback , AvatarImage } from "@/components/ui/avatar"
2- import { Badge } from "@/components/ui/badge"
32import { SheetTitle } from "@/components/ui/sheet"
43import he from "he"
54import { Music } from "lucide-react"
6- import { memo , useMemo } from "react"
5+ import { memo , useCallback , useEffect , useMemo , useRef , useState } from "react"
76import { MusicControls , ProgressBarMusic } from "../Common"
87
9- const NowPlayingTab = memo ( ( { currentSong, isDesktop = false } ) => {
8+ const useImageColors = ( imageSrc ) => {
9+ const [ colors , setColors ] = useState ( null )
10+ const canvasRef = useRef ( null )
11+
12+ const extractColors = useCallback ( ( src ) => {
13+ if ( ! src ) return
14+ const img = new Image ( )
15+ img . crossOrigin = "anonymous"
16+ img . onload = ( ) => {
17+ if ( ! canvasRef . current ) canvasRef . current = document . createElement ( "canvas" )
18+ const canvas = canvasRef . current
19+ const ctx = canvas . getContext ( "2d" , { willReadFrequently : true } )
20+ canvas . width = 32
21+ canvas . height = 32
22+ ctx . drawImage ( img , 0 , 0 , 32 , 32 )
23+ const data = ctx . getImageData ( 0 , 0 , 32 , 32 ) . data
24+
25+ const buckets = { }
26+ for ( let i = 0 ; i < data . length ; i += 8 ) {
27+ const r = data [ i ] , g = data [ i + 1 ] , b = data [ i + 2 ]
28+ const brightness = ( r + g + b ) / 3
29+ if ( brightness < 15 || brightness > 245 ) continue
30+ const sat = Math . max ( r , g , b ) - Math . min ( r , g , b )
31+ if ( sat < 12 ) continue
32+ const key = `${ Math . round ( r / 24 ) * 24 } ,${ Math . round ( g / 24 ) * 24 } ,${ Math . round ( b / 24 ) * 24 } `
33+ buckets [ key ] = ( buckets [ key ] || 0 ) + 1
34+ }
35+
36+ const sorted = Object . entries ( buckets ) . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] )
37+ const boost = ( c ) => c . map ( ( v ) => {
38+ const avg = ( c [ 0 ] + c [ 1 ] + c [ 2 ] ) / 3
39+ return Math . min ( 255 , Math . round ( v + ( v - avg ) * 0.5 + 30 ) )
40+ } )
41+
42+ const c1 = sorted [ 0 ] ? boost ( sorted [ 0 ] [ 0 ] . split ( "," ) . map ( Number ) ) : [ 120 , 80 , 200 ]
43+ const c2 = sorted [ 1 ] ? boost ( sorted [ 1 ] [ 0 ] . split ( "," ) . map ( Number ) ) : [ 200 , 80 , 120 ]
44+ const c3 = sorted [ 2 ] ? boost ( sorted [ 2 ] [ 0 ] . split ( "," ) . map ( Number ) ) : c1 . map ( ( v , i ) => Math . round ( ( v + c2 [ i ] ) / 2 ) )
45+
46+ setColors ( { c1, c2, c3 } )
47+ }
48+ img . src = src
49+ } , [ ] )
50+
51+ useEffect ( ( ) => {
52+ extractColors ( imageSrc )
53+ } , [ imageSrc , extractColors ] )
54+
55+ return colors
56+ }
57+
58+ const NowPlayingTab = memo ( ( { currentSong } ) => {
1059 const songImage = useMemo ( ( ) => currentSong ?. image ?. [ 2 ] ?. link , [ currentSong ] )
60+ const colors = useImageColors ( songImage )
1161
1262 const artistName = useMemo (
1363 ( ) =>
@@ -18,114 +68,91 @@ const NowPlayingTab = memo(({ currentSong, isDesktop = false }) => {
1868 [ currentSong ] ,
1969 )
2070
21- if ( isDesktop ) {
22- return (
23- < div className = "w-full h-full flex items-center justify-center px-8 py-12 relative overflow-hidden" >
24- < div
25- className = "absolute inset-0 bg-cover bg-center opacity-25 blur-3xl scale-125"
26- style = { { backgroundImage : `url(${ songImage } )` } }
27- />
28- < div className = "absolute inset-0 bg-linear-to-t from-background via-background/70 to-background/50" />
71+ const c1 = colors ?. c1 || [ 30 , 30 , 30 ]
72+ const c2 = colors ?. c2 || [ 20 , 20 , 20 ]
73+ const c3 = colors ?. c3 || [ 25 , 25 , 25 ]
2974
30- < div className = "relative z-10 flex flex-col items-center gap-12 w-full" >
31- < div
32- className = "relative group shrink-0"
33- style = { { width : "min(400px, 45vh)" , height : "min(400px, 45vh)" } }
34- >
35- < div className = "absolute inset-0 rounded-2xl bg-linear-to-br from-primary/30 via-transparent to-primary/20 opacity-60 blur-2xl scale-110 group-hover:opacity-80 transition-opacity duration-300" />
36- < Avatar className = "w-full h-full rounded-2xl shadow-2xl relative z-10 ring-1 ring-white/20" >
37- < AvatarImage src = { songImage } alt = { currentSong . name } className = "object-cover" />
38- < AvatarFallback className = "text-6xl bg-linear-to-br from-primary/10 to-primary/5" >
39- < Music className = "w-24 h-24 text-muted-foreground" />
40- </ AvatarFallback >
41- </ Avatar >
42- { currentSong ?. genre && (
43- < div className = "absolute top-4 left-4 z-20" >
44- < Badge
45- variant = "secondary"
46- className = "text-xs bg-black/40 backdrop-blur-xs border border-white/20 text-white"
47- >
48- { currentSong . genre }
49- </ Badge >
50- </ div >
51- ) }
52- </ div >
75+ const bgGradient = useMemo ( ( ) => `
76+ radial-gradient(ellipse 80% 80% at 15% 10%, rgba(${ c1 [ 0 ] } ,${ c1 [ 1 ] } ,${ c1 [ 2 ] } ,0.7) 0%, transparent 60%),
77+ radial-gradient(ellipse 70% 90% at 85% 85%, rgba(${ c2 [ 0 ] } ,${ c2 [ 1 ] } ,${ c2 [ 2 ] } ,0.6) 0%, transparent 55%),
78+ radial-gradient(ellipse 60% 60% at 55% 45%, rgba(${ c3 [ 0 ] } ,${ c3 [ 1 ] } ,${ c3 [ 2 ] } ,0.4) 0%, transparent 50%),
79+ radial-gradient(ellipse 90% 50% at 30% 90%, rgba(${ c2 [ 0 ] } ,${ c2 [ 1 ] } ,${ c2 [ 2 ] } ,0.35) 0%, transparent 55%),
80+ radial-gradient(ellipse 50% 80% at 80% 20%, rgba(${ c1 [ 0 ] } ,${ c1 [ 1 ] } ,${ c1 [ 2 ] } ,0.3) 0%, transparent 50%),
81+ black
82+ ` , [ c1 , c2 , c3 ] )
5383
54- < div className = "flex-1 flex flex-col gap-6 text-center" >
55- < div >
56- < SheetTitle className = "text-2xl xl:text-3xl font-bold mb-3 line-clamp-2" >
57- { he . decode ( currentSong . name ) }
58- </ SheetTitle >
59- < p className = "text-lg text-muted-foreground line-clamp-1" > { he . decode ( artistName ) } </ p >
60- { currentSong ?. album ?. name && (
61- < p className = "text-sm text-muted-foreground/60 mt-2" >
62- { he . decode ( currentSong . album . name ) }
63- </ p >
64- ) }
65- </ div >
66- < ProgressBarMusic isTimeVisible = { true } />
67-
68- < div className = "flex justify-center" >
69- < MusicControls size = "large" />
70- </ div >
71- </ div >
72- </ div >
73- </ div >
74- )
75- }
84+ const glowBg = useMemo (
85+ ( ) => `radial-gradient(circle, rgba(${ c1 [ 0 ] } ,${ c1 [ 1 ] } ,${ c1 [ 2 ] } ,0.5) 0%, transparent 70%)` ,
86+ [ c1 ] ,
87+ )
7688
7789 return (
78- < div className = "h-full flex flex-col p-4 sm:p-6 max-w-2xl mx-auto gap-10 relative" >
79- < div className = "absolute inset-0 bg-linear-to-b from-transparent via-primary/2 to-transparent pointer-events-none" />
90+ < div className = "w-full h-full flex items-center justify-center relative overflow-hidden bg-black" >
91+ < div
92+ className = "absolute inset-0 np-gradient-bg"
93+ style = { { background : bgGradient } }
94+ />
8095
81- < div className = "flex justify-center relative z-10" >
82- < div className = "w-full h-96 relative group" >
83- < div className = "absolute inset-0 rounded-2xl bg-linear-to-br from-primary/20 via-transparent to-primary/10 opacity-50 blur-xl scale-105 group-hover:opacity-70 transition-opacity duration-300" />
84- < Avatar className = "w-full h-full rounded-2xl shadow-2xl relative z-10 ring-1 ring-white/10" >
96+ < div className = "absolute inset-0 bg-black/25" />
97+
98+ < div className = "relative z-10 flex flex-col items-center w-full h-full justify-center px-6 sm:px-10 py-20 gap-6 sm:gap-8 max-w-xl mx-auto" >
99+ < div className = "relative group shrink-0" >
100+ < div
101+ className = "absolute -inset-6 rounded-3xl opacity-60 group-hover:opacity-80 transition-opacity duration-500"
102+ style = { { background : glowBg } }
103+ />
104+ < Avatar
105+ className = "rounded-2xl relative z-10"
106+ style = { {
107+ width : "min(85vw, 50vh, 520px)" ,
108+ height : "min(85vw, 50vh, 520px)" ,
109+ boxShadow : `0 8px 40px rgba(0,0,0,0.5), 0 0 80px rgba(${ c1 [ 0 ] } ,${ c1 [ 1 ] } ,${ c1 [ 2 ] } ,0.2)` ,
110+ } }
111+ >
85112 < AvatarImage src = { songImage } alt = { currentSong . name } className = "object-cover" />
86- < AvatarFallback className = "text-4xl sm:text- 6xl bg-linear-to-br from-primary/10 to-primary/5 backdrop-blur-xs " >
87- < Music className = "w-16 h-16 sm:w-20 sm:h-20 text-muted-foreground " />
113+ < AvatarFallback className = "text-6xl bg-white/5 " >
114+ < Music className = "w-24 h-24 text-white/30 " />
88115 </ AvatarFallback >
89116 </ Avatar >
90-
91- { currentSong ?. genre && (
92- < div className = "absolute top-3 left-3 z-20" >
93- < Badge
94- variant = "secondary"
95- className = "text-xs bg-white/20 backdrop-blur-xs border border-white/20 text-foreground"
96- >
97- { currentSong . genre }
98- </ Badge >
99- </ div >
100- ) }
101117 </ div >
102- </ div >
103118
104- < div className = "flex items-start justify-between relative z-10" >
105- < div className = "flex-1 min-w-0 pr-4" >
106- < SheetTitle className = "text-xl sm:text-2xl lg:text-3xl mb-1 line-clamp-2" >
107- { he . decode ( currentSong . name ) }
108- </ SheetTitle >
109- < p className = "text-sm sm:text-base text-muted-foreground line-clamp-1" >
110- { he . decode ( artistName ) }
111- </ p >
112- { currentSong ?. album ?. name && (
113- < p className = "text-xs sm:text-sm text-muted-foreground/70 mt-1" >
114- { he . decode ( currentSong . album . name ) }
115- </ p >
116- ) }
117- </ div >
118- </ div >
119+ < div className = "flex flex-col gap-5 text-center w-full" >
120+ < div >
121+ < SheetTitle className = "text-2xl sm:text-3xl font-bold mb-2 line-clamp-2 text-white drop-shadow-lg" >
122+ { he . decode ( currentSong . name ) }
123+ </ SheetTitle >
124+ < p className = "text-sm sm:text-base text-white/60 line-clamp-1 font-medium" > { he . decode ( artistName ) } </ p >
125+ { currentSong ?. album ?. name && (
126+ < p className = "text-xs sm:text-sm text-white/35 mt-1.5 font-light" >
127+ { he . decode ( currentSong . album . name ) }
128+ </ p >
129+ ) }
130+ </ div >
119131
120- < div >
121- < ProgressBarMusic isTimeVisible = { true } />
122- </ div >
132+ < ProgressBarMusic isTimeVisible = { true } />
123133
124- < div className = "space-y-6 relative z-10 " >
125- < div className = "flex justify-center" >
126- < MusicControls size = "large" / >
134+ < div className = "flex justify-center " >
135+ < MusicControls size = "large" / >
136+ </ div >
127137 </ div >
128138 </ div >
139+
140+ < style > { `
141+ .np-gradient-bg {
142+ transition: background 2s ease;
143+ animation: npShift 20s ease-in-out infinite alternate;
144+ }
145+ @keyframes npShift {
146+ 0% { filter: hue-rotate(0deg) brightness(1); transform: scale(1); }
147+ 25% { filter: hue-rotate(8deg) brightness(1.1); transform: scale(1.05); }
148+ 50% { filter: hue-rotate(-5deg) brightness(0.95); transform: scale(1.02); }
149+ 75% { filter: hue-rotate(12deg) brightness(1.05); transform: scale(1.08); }
150+ 100% { filter: hue-rotate(-8deg) brightness(1); transform: scale(1); }
151+ }
152+ @media (prefers-reduced-motion: reduce) {
153+ .np-gradient-bg { animation: none; }
154+ }
155+ ` } </ style >
129156 </ div >
130157 )
131158} )
0 commit comments