Skip to content

Commit 629f021

Browse files
committed
refactor: unify glassmorphism CSS order, optimize bottom player layout, and implement group playback state synchronization and keyboard controls.
1 parent 4303696 commit 629f021

9 files changed

Lines changed: 171 additions & 74 deletions

File tree

client/src/Context/GroupMusicContext.jsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,28 @@ export function GroupMusicProvider({ children }) {
5353
return () => window.removeEventListener("beforeunload", handleBeforeUnload)
5454
}, [])
5555

56+
useEffect(() => {
57+
const handleKeyDown = (e) => {
58+
const activeGroup = useGroupSessionStore.getState().currentGroup
59+
if (!activeGroup) return
60+
61+
if (
62+
e.key === " " &&
63+
document.activeElement &&
64+
!["INPUT", "TEXTAREA"].includes(document.activeElement.tagName)
65+
) {
66+
e.preventDefault()
67+
playback.handlePlayPause(socket, activeGroup.id)
68+
}
69+
if (e.key === "ArrowRight") {
70+
session.skipSong(socket, user)
71+
}
72+
}
73+
74+
window.addEventListener("keydown", handleKeyDown)
75+
return () => window.removeEventListener("keydown", handleKeyDown)
76+
}, [socket, user, playback, session])
77+
5678
useEffect(() => {
5779
if (!socket) return
5880

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const DraggableButton = memo(({ position, onMaximize, currentSong, isDragging })
7777
top: transform ? position.y + transform.y : position.y,
7878
left: transform ? position.x + transform.x : position.x,
7979
touchAction: "none",
80-
zIndex: 9999,
80+
zIndex: 10,
8181
}
8282

8383
const artists = useMemo(() => {

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@ import { memo, useEffect } from "react"
44
import { Button } from "@/components/ui/button"
55
import { cn } from "@/lib/utils"
66
import { usePlayerStore } from "@/stores/playerStore"
7+
import { useGroupSessionStore } from "@/stores/groupMusic/sessionStore"
78
import { MusicControls, VolumeControl } from "../Common"
89
import SleepTimerModal from "../SleepTimer"
910

1011
const PlayerControls = memo(({ onMinimize, onOpenModal, isMobile }) => {
1112
const handlePlayPause = usePlayerStore((s) => s.handlePlayPause)
1213
const handleNextSong = usePlayerStore((s) => s.handleNextSong)
1314
const handlePrevSong = usePlayerStore((s) => s.handlePrevSong)
15+
const isGroupActive = useGroupSessionStore((s) => !!s.currentGroup)
1416

1517
useEffect(() => {
1618
const handleKeyDown = (e) => {
19+
if (isGroupActive) return
20+
1721
if (
1822
e.key === " " &&
1923
document.activeElement &&
@@ -28,7 +32,7 @@ const PlayerControls = memo(({ onMinimize, onOpenModal, isMobile }) => {
2832

2933
window.addEventListener("keydown", handleKeyDown)
3034
return () => window.removeEventListener("keydown", handleKeyDown)
31-
}, [handlePlayPause, handleNextSong, handlePrevSong])
35+
}, [isGroupActive, handlePlayPause, handleNextSong, handlePrevSong])
3236

3337
return (
3438
<div className="flex items-center gap-1 sm:gap-2">

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

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useIsMobile } from "@/hooks/use-mobile"
55
import { cn } from "@/lib/utils"
66
import { usePlayerStore } from "@/stores/playerStore"
77
import { useAppModeStore } from "@/stores/appModeStore"
8+
import { useGroupPlaybackStore } from "@/stores/groupMusic/playbackStore"
89
import AddToPlaylist from "../AddToPlaylist"
910
import { ProgressBarMusic } from "../Common"
1011
import MinimizedPlayer from "./MinimizedPlayer"
@@ -17,6 +18,15 @@ const BottomPlayer = () => {
1718
const playlist = usePlayerStore((s) => s.playlist)
1819
const addToQueue = usePlayerStore((s) => s.addToQueue)
1920
const autoFetchRecommendations = usePlayerStore((s) => s.autoFetchRecommendations)
21+
const isSoloPlaying = usePlayerStore((s) => s.isPlaying)
22+
const handlePlayPause = usePlayerStore((s) => s.handlePlayPause)
23+
const isGroupPlaying = useGroupPlaybackStore((s) => s.isPlaying)
24+
25+
useEffect(() => {
26+
if (isGroupPlaying && isSoloPlaying) {
27+
handlePlayPause()
28+
}
29+
}, [isGroupPlaying, isSoloPlaying, handlePlayPause])
2030

2131
const isMobile = useIsMobile()
2232
const appMode = useAppModeStore((s) => s.mode)
@@ -60,40 +70,47 @@ const BottomPlayer = () => {
6070

6171
return (
6272
<>
63-
<Card
73+
<div
6474
className={cn(
65-
"fixed left-0 w-full border-0 liquid-glass z-50 transition-all duration-300 ease-out overflow-hidden",
66-
hasMobileNav ? "bottom-14" : "bottom-0",
75+
"fixed left-0 w-full transition-all duration-300 ease-out",
76+
hasMobileNav ? "bottom-0 z-40" : "bottom-0 z-50",
6777
isMinimized
6878
? "translate-y-full opacity-0 pointer-events-none"
6979
: "translate-y-0 opacity-100",
7080
)}
71-
style={{ borderRadius: 0 }}
7281
>
73-
{/* Ambient Background Layer */}
74-
<div className="absolute inset-0 -z-10 overflow-hidden pointer-events-none opacity-40">
75-
<img
76-
src={songImage}
77-
alt=""
78-
className="w-full h-full object-cover blur-2xl scale-150 transition-all duration-1000"
79-
/>
82+
<div className="absolute -top-1 left-0 right-0 z-50">
83+
<ProgressBarMusic />
8084
</div>
8185

82-
<CardContent className="p-0 relative">
83-
<div className="absolute top-0 left-0 right-0">
84-
<ProgressBarMusic />
85-
</div>
86-
87-
<div className="flex items-center justify-between px-4 py-3 pt-4">
88-
<SongInfo currentSong={currentSong} onOpenSheet={() => setIsSheetOpen(true)} />
89-
<PlayerControls
90-
onMinimize={() => setIsMinimized(true)}
91-
onOpenModal={() => setIsModalOpen(true)}
92-
isMobile={isMobile}
86+
<Card
87+
className="w-full border-0 liquid-glass overflow-hidden"
88+
style={{
89+
borderRadius: 0,
90+
background: hasMobileNav ? "rgba(10, 12, 18, 0.9)" : undefined,
91+
}}
92+
>
93+
{/* Ambient Background Layer */}
94+
<div className="absolute inset-0 -z-10 overflow-hidden pointer-events-none opacity-40">
95+
<img
96+
src={songImage}
97+
alt=""
98+
className="w-full h-full object-cover blur-2xl scale-150 transition-all duration-1000"
9399
/>
94100
</div>
95-
</CardContent>
96-
</Card>
101+
102+
<CardContent className={cn("p-0 relative pt-1.5", hasMobileNav && "pb-14")}>
103+
<div className="flex items-center justify-between px-4 py-3">
104+
<SongInfo currentSong={currentSong} onOpenSheet={() => setIsSheetOpen(true)} />
105+
<PlayerControls
106+
onMinimize={() => setIsMinimized(true)}
107+
onOpenModal={() => setIsModalOpen(true)}
108+
isMobile={isMobile}
109+
/>
110+
</div>
111+
</CardContent>
112+
</Card>
113+
</div>
97114

98115
<MinimizedPlayer
99116
isMinimized={isMinimized}

client/src/Pages/Music/Common.jsx

Lines changed: 80 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
Volume2,
1414
VolumeX,
1515
} from "lucide-react"
16-
import { memo, useCallback, useState } from "react"
16+
import { memo, useCallback, useState, useRef } from "react"
1717
import ShareDrawer from "@/components/Posts/ShareDrawer"
1818
import { Button } from "@/components/ui/button"
1919
import { Slider } from "@/components/ui/slider"
@@ -63,9 +63,8 @@ export const PlaylistActions = ({ onPlayAll, onShuffle, disabled, showShare = tr
6363

6464
export const LoadingState = ({ message, height }) => (
6565
<div
66-
className={`flex ${
67-
height ? height : "h-full"
68-
} items-center justify-center bg-background/50 backdrop-blur-xs`}
66+
className={`flex ${height ? height : "h-full"
67+
} items-center justify-center bg-background/50 backdrop-blur-xs`}
6968
>
7069
<div className="flex flex-col items-center gap-4">
7170
<Loader2 className="w-10 h-10 animate-spin text-emerald-500" />
@@ -266,23 +265,71 @@ export const ProgressBarMusic = memo(({ isTimeVisible = false }) => {
266265
const currentTime = usePlayerStore((s) => s.currentTime)
267266
const duration = usePlayerStore((s) => s.duration)
268267
const handleTimeSeek = usePlayerStore((s) => s.handleTimeSeek)
268+
const [hoverTime, setHoverTime] = useState(null)
269+
const [hoverX, setHoverX] = useState(0)
270+
const [isDragging, setIsDragging] = useState(false)
271+
const containerRef = useRef(null)
272+
273+
const handleMouseMove = (e) => {
274+
const rect = e.currentTarget.getBoundingClientRect()
275+
const x = e.clientX - rect.left
276+
const pct = Math.max(0, Math.min(1, x / rect.width))
277+
setHoverTime(pct * duration)
278+
setHoverX(x)
279+
}
280+
281+
const handleMouseLeave = () => {
282+
setHoverTime(null)
283+
}
284+
285+
const showTooltip = (hoverTime !== null || isDragging) && duration > 0
286+
const displayTime = isDragging ? currentTime : hoverTime
287+
288+
let activeX = hoverX
289+
if (isDragging && containerRef.current) {
290+
const rect = containerRef.current.getBoundingClientRect()
291+
const pct = currentTime / (duration || 1)
292+
activeX = pct * rect.width
293+
}
269294

270295
return (
271-
<div className="space-y-1.5 group/progress">
296+
<div
297+
ref={containerRef}
298+
className="relative group/progress cursor-pointer overflow-visible"
299+
onMouseMove={handleMouseMove}
300+
onMouseLeave={handleMouseLeave}
301+
>
302+
{showTooltip && (
303+
<div
304+
className="absolute z-[100] bg-[#0a0c12]/95 border border-white/10 text-white text-[10px] font-semibold px-2 py-0.5 rounded shadow-xl pointer-events-none -translate-x-1/2"
305+
style={{
306+
left: `${activeX}px`,
307+
bottom: "18px",
308+
}}
309+
>
310+
{formatTime(displayTime)}
311+
</div>
312+
)}
313+
272314
<Slider
273315
value={[currentTime]}
274316
min={0}
275317
max={duration || 1}
276318
step={0.1}
277319
onValueChange={([value]) => handleTimeSeek(value)}
320+
onPointerDown={() => setIsDragging(true)}
321+
onPointerUp={() => setIsDragging(false)}
322+
onPointerCancel={() => setIsDragging(false)}
278323
className={cn(
279-
"h-1.5 cursor-pointer transition-all duration-200",
280-
"[&_[data-slider-track]]:h-[5px] [&_[data-slider-track]]:bg-white/15 [&_[data-slider-track]]:rounded-full",
281-
"[&_[data-slider-range]]:bg-white [&_[data-slider-range]]:rounded-full",
282-
"**:[[role=slider]]:h-4 **:[[role=slider]]:w-4 **:[[role=slider]]:bg-white **:[[role=slider]]:border-0",
283-
"**:[[role=slider]]:shadow-[0_0_8px_rgba(255,255,255,0.3)]",
324+
"relative h-5 py-2 -my-2 cursor-pointer transition-colors duration-200",
325+
"[&>span:first-child]:h-[4px] [&>span:first-child]:bg-white/15 [&>span:first-child]:rounded-full [&>span:first-child]:transition-[height,background-color] [&>span:first-child]:duration-200",
326+
"group-hover/progress:[&>span:first-child]:h-[6px] group-hover/progress:[&>span:first-child]:bg-white/20",
327+
"[&>span:first-child>span]:bg-white [&>span:first-child>span]:rounded-full [&>span:first-child>span]:transition-colors [&>span:first-child>span]:duration-200",
328+
"group-hover/progress:[&>span:first-child>span]:bg-primary",
329+
"**:[[role=slider]]:h-3.5 **:[[role=slider]]:w-3.5 **:[[role=slider]]:bg-white **:[[role=slider]]:border-0",
330+
"**:[[role=slider]]:shadow-[0_0_10px_rgba(255,255,255,0.4)]",
284331
"**:[[role=slider]]:opacity-0 **:[[role=slider]]:scale-50",
285-
"**:[[role=slider]]:transition-all **:[[role=slider]]:duration-200",
332+
"**:[[role=slider]]:transition-[opacity,transform] **:[[role=slider]]:duration-200",
286333
"group-hover/progress:**:[[role=slider]]:opacity-100 group-hover/progress:**:[[role=slider]]:scale-100",
287334
)}
288335
/>
@@ -301,32 +348,32 @@ export const ensureHttpsForDownloadUrls = (song) => {
301348

302349
const updatedDownloadUrls = Array.isArray(song.download_url)
303350
? song.download_url.map((item) => {
304-
if (!item || typeof item !== "object") return item
305-
return {
306-
...item,
307-
link:
308-
item.link && typeof item.link === "string"
309-
? item.link.startsWith("http://")
310-
? item.link.replace("http://", "https://")
311-
: item.link
312-
: item.link,
313-
}
314-
})
351+
if (!item || typeof item !== "object") return item
352+
return {
353+
...item,
354+
link:
355+
item.link && typeof item.link === "string"
356+
? item.link.startsWith("http://")
357+
? item.link.replace("http://", "https://")
358+
: item.link
359+
: item.link,
360+
}
361+
})
315362
: song.download_url
316363

317364
const updatedArtworkUrls = Array.isArray(song.image)
318365
? song.image.map((item) => {
319-
if (!item || typeof item !== "object") return item
320-
return {
321-
...item,
322-
link:
323-
item.link && typeof item.link === "string"
324-
? item.link.startsWith("http://")
325-
? item.link.replace("http://", "https://")
326-
: item.link
327-
: item.link,
328-
}
329-
})
366+
if (!item || typeof item !== "object") return item
367+
return {
368+
...item,
369+
link:
370+
item.link && typeof item.link === "string"
371+
? item.link.startsWith("http://")
372+
? item.link.replace("http://", "https://")
373+
: item.link
374+
: item.link,
375+
}
376+
})
330377
: song.image
331378

332379
return {

0 commit comments

Comments
 (0)