Skip to content

Commit a9121c1

Browse files
committed
feat: Enhance music player UI with improved animations, volume control, and progress bar styling.
1 parent 264db87 commit a9121c1

8 files changed

Lines changed: 377 additions & 179 deletions

File tree

Lines changed: 100 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,54 @@
1-
import { memo, useMemo } from "react"
21
import { useDraggable } from "@dnd-kit/core"
3-
import { Card } from "@/components/ui/card"
4-
import { PlayIcon } from "lucide-react"
5-
import { usePlayerStore } from "@/stores/playerStore"
6-
import { AudioWave } from "../Cards"
72
import he from "he"
3+
import { Pause, Play } from "lucide-react"
4+
import { memo, useMemo } from "react"
5+
import { usePlayerStore } from "@/stores/playerStore"
6+
7+
const ProgressRing = memo(({ progress, size = 44, strokeWidth = 2.5 }) => {
8+
const radius = (size - strokeWidth) / 2
9+
const circumference = radius * 2 * Math.PI
10+
const strokeDashoffset = circumference - (progress / 100) * circumference
11+
12+
return (
13+
<svg className="progress-ring absolute inset-0" width={size} height={size}>
14+
<circle
15+
className="text-white/10"
16+
strokeWidth={strokeWidth}
17+
stroke="currentColor"
18+
fill="transparent"
19+
r={radius}
20+
cx={size / 2}
21+
cy={size / 2}
22+
/>
23+
<circle
24+
className="progress-ring-circle text-primary"
25+
strokeWidth={strokeWidth}
26+
strokeDasharray={circumference}
27+
strokeDashoffset={strokeDashoffset}
28+
stroke="currentColor"
29+
fill="transparent"
30+
r={radius}
31+
cx={size / 2}
32+
cy={size / 2}
33+
/>
34+
</svg>
35+
)
36+
})
37+
38+
const AudioWaveVisual = memo(() => (
39+
<div className="flex items-center gap-0.5 h-4">
40+
<div className="audio-bar" style={{ animationDelay: "0s" }} />
41+
<div className="audio-bar" style={{ animationDelay: "0.2s" }} />
42+
<div className="audio-bar" style={{ animationDelay: "0.4s" }} />
43+
</div>
44+
))
845

946
const DraggableButton = memo(({ position, onMaximize, currentSong, isDragging }) => {
1047
const isPlaying = usePlayerStore((s) => s.isPlaying)
48+
const currentTime = usePlayerStore((s) => s.currentTime)
49+
const duration = usePlayerStore((s) => s.duration)
50+
const handlePlayPause = usePlayerStore((s) => s.handlePlayPause)
51+
1152
const { attributes, listeners, setNodeRef, transform } = useDraggable({
1253
id: "minimized-player",
1354
})
@@ -19,33 +60,69 @@ const DraggableButton = memo(({ position, onMaximize, currentSong, isDragging })
1960
[currentSong],
2061
)
2162

63+
const progress = useMemo(() => {
64+
if (!duration) return 0
65+
return (currentTime / duration) * 100
66+
}, [currentTime, duration])
67+
2268
const style = {
2369
position: "fixed",
24-
top: position.y,
25-
left: position.x,
26-
transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : undefined,
70+
top: transform ? position.y + transform.y : position.y,
71+
left: transform ? position.x + transform.x : position.x,
2772
touchAction: "none",
73+
zIndex: 9999,
74+
cursor: isDragging ? "grabbing" : "grab",
2875
}
2976

3077
return (
31-
<Card
32-
ref={setNodeRef}
33-
{...attributes}
34-
{...listeners}
35-
onClick={() => !isDragging && onMaximize()}
36-
style={style}
37-
className="flex items-center gap-1 px-2 py-1 rounded-full shadow-md cursor-pointer select-none z-[9999]"
38-
>
39-
<div className="w-8 h-8 rounded-full overflow-hidden">
40-
<img src={songImage} alt="" className="w-full h-full object-cover" />
78+
<div ref={setNodeRef} {...attributes} {...listeners} style={style} className="select-none">
79+
<div
80+
className={`flex items-center gap-2 pl-1 pr-3 py-1 rounded-full bg-[#1a1a1a] border border-white/[0.08] transition-transform ${
81+
isDragging ? "scale-105" : ""
82+
}`}
83+
>
84+
<div
85+
className="relative w-11 h-11 flex-shrink-0 cursor-pointer"
86+
onClick={(e) => {
87+
e.stopPropagation()
88+
if (!isDragging) handlePlayPause()
89+
}}
90+
>
91+
<ProgressRing progress={progress} size={44} strokeWidth={2.5} />
92+
<div className="absolute inset-[3px] rounded-full overflow-hidden">
93+
<img
94+
src={songImage}
95+
alt=""
96+
className={`w-full h-full object-cover ${isPlaying ? "rotate-animation" : ""}`}
97+
/>
98+
</div>
99+
<div className="absolute inset-0 flex items-center justify-center bg-black/40 rounded-full opacity-0 hover:opacity-100 transition-opacity">
100+
{isPlaying ? (
101+
<Pause size={14} className="text-white" fill="currentColor" />
102+
) : (
103+
<Play size={14} className="text-white ml-0.5" fill="currentColor" />
104+
)}
105+
</div>
106+
</div>
107+
108+
<div
109+
onClick={(e) => {
110+
e.stopPropagation()
111+
if (!isDragging) onMaximize()
112+
}}
113+
className="flex items-center gap-2 cursor-pointer"
114+
>
115+
{isPlaying && <AudioWaveVisual />}
116+
<span className="text-sm text-white font-medium max-w-[80px] truncate">
117+
{he.decode(currentSong?.name || "")}
118+
</span>
119+
</div>
41120
</div>
42-
{isPlaying ? <AudioWave /> : <PlayIcon size={20} />}
43-
<span className="hidden sm:block text-sm max-w-[100px] truncate">
44-
{he.decode(currentSong?.name || "")}
45-
</span>
46-
</Card>
121+
</div>
47122
)
48123
})
49124

125+
ProgressRing.displayName = "ProgressRing"
126+
AudioWaveVisual.displayName = "AudioWaveVisual"
50127
DraggableButton.displayName = "DraggableButton"
51128
export default DraggableButton

client/src/Pages/Music/BottomPlayer/MinimizedPlayer.jsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,29 @@ import { memo, useEffect, useState } from "react"
22
import { DndContext, PointerSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core"
33
import DraggableButton from "./DraggableButton"
44

5-
const BUTTON_SIZE = 48
5+
const BUTTON_SIZE = 56
66

77
const MinimizedPlayer = memo(({ isMinimized, onMaximize, currentSong, isMobile }) => {
88
const [position, setPosition] = useState({
9-
x: isMobile ? window.innerWidth - 100 : 23,
10-
y: isMobile ? window.innerHeight - 150 : 752,
9+
x: isMobile ? window.innerWidth - 180 : 23,
10+
y: isMobile ? window.innerHeight - 100 : window.innerHeight - 120,
1111
})
1212
const [isDragging, setIsDragging] = useState(false)
1313

1414
const sensors = useSensors(
1515
useSensor(PointerSensor, {
1616
activationConstraint: {
17-
distance: 8,
17+
distance: 5,
1818
},
1919
}),
2020
useSensor(TouchSensor, {
2121
activationConstraint: {
22-
delay: 250,
23-
tolerance: 8,
22+
delay: 150,
23+
tolerance: 5,
2424
},
2525
}),
2626
)
2727

28-
// Handle window resize
2928
useEffect(() => {
3029
const handleResize = () => {
3130
setPosition((prevPos) => ({
@@ -43,7 +42,6 @@ const MinimizedPlayer = memo(({ isMinimized, onMaximize, currentSong, isMobile }
4342
}
4443

4544
const handleDragEnd = (event) => {
46-
setIsDragging(false)
4745
const { delta } = event
4846

4947
if (delta) {
@@ -52,6 +50,8 @@ const MinimizedPlayer = memo(({ isMinimized, onMaximize, currentSong, isMobile }
5250
y: Math.min(Math.max(0, prev.y + delta.y), window.innerHeight - BUTTON_SIZE),
5351
}))
5452
}
53+
54+
setIsDragging(false)
5555
}
5656

5757
if (!isMinimized) return null

client/src/Pages/Music/BottomPlayer/PlayerControls.jsx

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
import { motion } from "framer-motion"
2+
import { ChevronDown, ListMusic } from "lucide-react"
13
import { memo, useEffect } from "react"
24
import { Button } from "@/components/ui/button"
3-
import { ListMusic, Minimize2 } from "lucide-react"
4-
import { usePlayerStore } from "@/stores/playerStore"
55
import { cn } from "@/lib/utils"
6+
import { usePlayerStore } from "@/stores/playerStore"
67
import { MusicControls, VolumeControl } from "../Common"
78
import SleepTimerModal from "../SleepTimer"
89

9-
const PlayerControls = memo(({ isMinimized, onMinimize, onOpenModal, isMobile }) => {
10-
// Individual selectors - actions are stable references
10+
const PlayerControls = memo(({ onMinimize, onOpenModal }) => {
1111
const handlePlayPause = usePlayerStore((s) => s.handlePlayPause)
1212
const handleNextSong = usePlayerStore((s) => s.handleNextSong)
1313
const handlePrevSong = usePlayerStore((s) => s.handlePrevSong)
@@ -31,37 +31,39 @@ const PlayerControls = memo(({ isMinimized, onMinimize, onOpenModal, isMobile })
3131
}, [handlePlayPause, handleNextSong, handlePrevSong])
3232

3333
return (
34-
<div className="flex items-center gap-2">
35-
<Button
36-
variant="ghost"
37-
size="icon"
38-
className={cn(
39-
"h-12 w-12 rounded-full shadow-lg",
40-
"transition-all duration-500 hover:scale-105",
41-
isMinimized
42-
? "opacity-0 pointer-events-none -translate-y-10"
43-
: "opacity-100 translate-y-0",
44-
)}
45-
onClick={onMinimize}
46-
>
47-
<Minimize2 size={20} />
48-
</Button>
34+
<div className="flex items-center gap-1 sm:gap-2">
35+
<MusicControls />
4936

50-
<Button
51-
variant="ghost"
52-
size="icon"
53-
onClick={(e) => {
54-
e.stopPropagation()
55-
onOpenModal()
56-
}}
57-
className="hidden sm:flex hover:scale-105"
58-
>
59-
<ListMusic size={18} />
60-
</Button>
37+
<div className="hidden sm:flex items-center gap-1">
38+
<VolumeControl showVolume={true} />
39+
</div>
6140

62-
<VolumeControl />
63-
{!isMobile && <SleepTimerModal />}
64-
<MusicControls />
41+
<SleepTimerModal />
42+
43+
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
44+
<Button
45+
variant="ghost"
46+
size="icon"
47+
onClick={(e) => {
48+
e.stopPropagation()
49+
onOpenModal()
50+
}}
51+
className="hidden sm:flex h-9 w-9 hover:bg-white/10 text-white/70 hover:text-white"
52+
>
53+
<ListMusic size={18} />
54+
</Button>
55+
</motion.div>
56+
57+
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
58+
<Button
59+
variant="ghost"
60+
size="icon"
61+
className={cn("h-9 w-9 hover:bg-white/10 text-white/70 hover:text-white")}
62+
onClick={onMinimize}
63+
>
64+
<ChevronDown size={18} />
65+
</Button>
66+
</motion.div>
6567
</div>
6668
)
6769
})

client/src/Pages/Music/BottomPlayer/SongInfo.jsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import he from "he"
2+
import { Music } from "lucide-react"
13
import { memo, useMemo } from "react"
24
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
3-
import he from "he"
45

56
const SongInfo = memo(({ currentSong, onOpenSheet }) => {
67
const songImage = useMemo(
@@ -19,23 +20,34 @@ const SongInfo = memo(({ currentSong, onOpenSheet }) => {
1920
[currentSong],
2021
)
2122

23+
const decodedName = useMemo(() => he.decode(currentSong?.name || ""), [currentSong])
24+
const decodedArtist = useMemo(() => he.decode(artistName), [artistName])
25+
2226
return (
23-
<div
24-
className="flex items-center gap-4 flex-1 min-w-0 cursor-pointer"
27+
<button
28+
type="button"
29+
className="flex items-center gap-3 sm:gap-4 flex-1 min-w-0 cursor-pointer group text-left bg-transparent border-none p-0"
2530
onClick={(e) => {
2631
e.stopPropagation()
2732
onOpenSheet()
2833
}}
2934
>
30-
<Avatar className="h-14 w-14 rounded-md">
31-
<AvatarImage src={songImage} alt={currentSong.name} />
32-
<AvatarFallback>MU</AvatarFallback>
33-
</Avatar>
34-
<div className="flex flex-col min-w-0">
35-
<p className="text-sm font-medium truncate">{he.decode(currentSong.name)}</p>
36-
<p className="text-xs text-muted-foreground truncate">{he.decode(artistName)}</p>
35+
<div className="relative">
36+
<Avatar className="h-12 w-12 sm:h-14 sm:w-14 rounded-lg relative ring-1 ring-white/10">
37+
<AvatarImage src={songImage} alt={currentSong?.name} className="object-cover" />
38+
<AvatarFallback className="bg-white/5">
39+
<Music className="w-5 h-5 text-white/40" />
40+
</AvatarFallback>
41+
</Avatar>
42+
</div>
43+
44+
<div className="flex flex-col min-w-0 flex-1 max-w-[140px] sm:max-w-[200px]">
45+
<div className="overflow-hidden">
46+
<p className="text-sm font-semibold text-white truncate">{decodedName}</p>
47+
</div>
48+
<p className="text-xs text-white/50 truncate">{decodedArtist}</p>
3749
</div>
38-
</div>
50+
</button>
3951
)
4052
})
4153

client/src/Pages/Music/BottomPlayer/index.jsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,20 @@ const BottomPlayer = () => {
4545
<>
4646
<Card
4747
className={cn(
48-
"fixed bottom-0 left-0 w-full bg-background/80 backdrop-blur-md border-t z-50 transition-all duration-500",
49-
isMinimized ? "translate-y-full opacity-0" : "translate-y-0 opacity-100",
48+
"fixed bottom-0 left-0 w-full border-0 bg-background/95 backdrop-blur-md z-50 transition-all duration-300 ease-out",
49+
isMinimized
50+
? "translate-y-full opacity-0 pointer-events-none"
51+
: "translate-y-0 opacity-100",
5052
)}
5153
>
5254
<CardContent className="p-0">
53-
<div className="absolute -top-1 left-0 w-full">
55+
<div className="absolute top-0 left-0 right-0">
5456
<ProgressBarMusic />
5557
</div>
5658

57-
<div className="flex items-center justify-between p-4 pt-5">
59+
<div className="flex items-center justify-between px-4 py-3 pt-4">
5860
<SongInfo currentSong={currentSong} onOpenSheet={() => setIsSheetOpen(true)} />
59-
6061
<PlayerControls
61-
isMinimized={isMinimized}
6262
onMinimize={() => setIsMinimized(true)}
6363
onOpenModal={() => setIsModalOpen(true)}
6464
isMobile={isMobile}

0 commit comments

Comments
 (0)