setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+ {/* Glass Container */}
+
-
-
-
- {currentTrack.title}
-
- WAV
-
-
- {typeof currentTrack.artist === 'string' ? currentTrack.artist : 'Unknown Artist'}
-
+ {/* Progress Bar (Top Edge) */}
+
{
+ if (isIdle) return;
+ const rect = e.currentTarget.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const pct = x / rect.width;
+ player.seek(pct * player.duration);
+ }}>
+
- {/* CENTER: Controls */}
-
-
-
+
-
-
-
-
- {/* Progress Slider */}
-
- {formatTime((progress / 100) * (typeof duration === 'number' ? duration : 0))}
- seek(val[0])}
- max={100}
- step={0.1}
- className="h-4 py-1.5"
- />
- {formatTime(typeof duration === 'number' ? duration : 0)}
-
-
-
- {/* RIGHT: Volume & Extras */}
-
- {/* Visualization (Hidden on small screens) */}
-
-
-
-
-
-
setVolume(volume === 0 ? 50 : 0)} className="p-1.5 text-muted-foreground hover:text-white transition-colors">
- {volume === 0 ? : }
-
-
-
setVolume(val[0])} max={100} step={1} />
+
+
+
!isIdle && setIsExpanded(true)}>
+ {displayTrack.title}
+
+ {/* Quality Badge - Show only if not idle */}
+ {!isIdle && (
+
+ HQ
+
+ )}
+
+
!isIdle && setIsExpanded(true)}>
+ {displayTrack.artist || 'Unknown Artist'}
+
-
-
-
+ {/* CENTER: Controls */}
+
+
{
+ if (player.isPlaying) player.pause();
+ else if (!isIdle) player.resume();
+ }}
+ onNext={player.next}
+ onPrevious={player.previous}
+ onShuffle={player.toggleShuffle}
+ onRepeat={() => {
+ const modes = ['off', 'track', 'playlist'] as const; // Should cycle
+ const next = modes[(modes.indexOf(player.repeat) + 1) % modes.length];
+ player.setRepeat(next);
+ }}
+ shuffle={player.shuffle}
+ repeat={player.repeat}
+ />
+
+ {formatTime(player.currentTime)}
+ /
+ {formatTime(player.duration)}
+
+
+
+ {/* RIGHT: Actions & Volume */}
+
+ {/* Waveform Visualization (Desktop) */}
+
+
+
+
+ {/* Volume */}
+
+
+ {player.muted || player.volume === 0 ? : }
+
+
+ player.setVolume(val[0])}
+ max={100}
+ className="w-20"
+ />
+
+
+
+
+
+
setShowQueue(!showQueue)}
+ >
+
+
+
+
+
+
+
+
+ {/* Ambient Glow */}
+
-
- {/* Glow Effect Background */}
-
-
+ >
);
}
diff --git a/apps/web/src/features/player/components/PlayerControls.tsx b/apps/web/src/features/player/components/PlayerControls.tsx
new file mode 100644
index 000000000..53f69a67e
--- /dev/null
+++ b/apps/web/src/features/player/components/PlayerControls.tsx
@@ -0,0 +1,88 @@
+import { Play, Pause, SkipBack, SkipForward, Shuffle, Repeat } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+
+export interface PlayerControlsProps {
+ isPlaying: boolean;
+ onPlayPause: () => void;
+ onNext: () => void;
+ onPrevious: () => void;
+ onShuffle: () => void;
+ onRepeat: () => void;
+ shuffle: boolean;
+ repeat: 'off' | 'track' | 'playlist';
+ isExpanded?: boolean;
+}
+
+export function PlayerControls({
+ isPlaying,
+ onPlayPause,
+ onNext,
+ onPrevious,
+ onShuffle,
+ onRepeat,
+ shuffle,
+ repeat,
+ isExpanded = false
+}: PlayerControlsProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ {isPlaying ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+ {repeat === 'track' && (
+ 1
+ )}
+
+
+ );
+}
diff --git a/apps/web/src/features/player/components/PlayerExpanded.tsx b/apps/web/src/features/player/components/PlayerExpanded.tsx
new file mode 100644
index 000000000..5d297e434
--- /dev/null
+++ b/apps/web/src/features/player/components/PlayerExpanded.tsx
@@ -0,0 +1,157 @@
+import React from 'react';
+import { usePlayerStore } from '../store/playerStore';
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import { Slider } from '@/components/ui/slider';
+import {
+ ChevronDown, Heart, MoreHorizontal, Share2,
+ MessageSquare, Mic2, FileText, Music2
+} from 'lucide-react';
+import { PlayPauseButton } from './PlayPauseButton'; // We might reuse or inline for consistent style
+import { NextPreviousButtons } from './NextPreviousButtons';
+import { RepeatShuffleButtons } from './RepeatShuffleButtons';
+
+interface PlayerExpandedProps {
+ isOpen: boolean;
+ onClose: () => void;
+ currentTime: number;
+ duration: number;
+ onSeek: (time: number) => void;
+ player: any; // Using the player hook object
+}
+
+export function PlayerExpanded({ isOpen, onClose, currentTime, duration, onSeek, player }: PlayerExpandedProps) {
+ const { currentTrack } = usePlayerStore();
+
+ if (!isOpen || !currentTrack) return null;
+
+ const formatTime = (seconds: number) => {
+ if (!seconds && seconds !== 0) return '0:00';
+ const m = Math.floor(seconds / 60);
+ const s = Math.floor(seconds % 60);
+ return `${m}:${s.toString().padStart(2, '0')}`;
+ };
+
+ return (
+
+ {/* Dynamic Background */}
+
+
+ {/* Header */}
+
+
+
+
+ Following the Signal
+
+
+
+
+
+ {/* Main Content */}
+
+
+ {/* Left: Album Art */}
+
+
+

+
+
+ {/* Right: Info & Controls */}
+
+
+
+
+
+ {currentTrack.title}
+
+
+ {currentTrack.artist}
+
+
+
+
+
+
+
+ {/* Progress */}
+
+
onSeek(val[0])}
+ max={duration || 100}
+ step={0.1}
+ className="py-2"
+ />
+
+ {formatTime(currentTime)}
+ {formatTime(duration)}
+
+
+
+ {/* Controls */}
+
+
+ {/* Todo: Reuse or reimplement buttons with larger sizes */}
+
+
+
+
+
+
player.isPlaying ? player.pause() : player.resume()}
+ size="xl" // We need to support 'xl' maybe or modify the component
+ className="scale-125"
+ />
+
+ {/* Wait, NextPrevious contains both buttons. I was using it wrong above. */}
+
+
+
+
+
+
+ {/* Lyrics toggle placeholder */}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/features/player/components/PlayerQueue.tsx b/apps/web/src/features/player/components/PlayerQueue.tsx
new file mode 100644
index 000000000..675333bf4
--- /dev/null
+++ b/apps/web/src/features/player/components/PlayerQueue.tsx
@@ -0,0 +1,134 @@
+import React from 'react';
+import { usePlayerStore } from '../store/playerStore';
+import { cn } from '@/lib/utils';
+import { X, GripVertical, AlertCircle } from 'lucide-react';
+import { Badge } from '@/components/ui/badge';
+import { ScrollArea } from '@/components/ui/scroll-area';
+
+interface PlayerQueueProps {
+ isOpen: boolean;
+ onClose: () => void;
+ currentTrackId?: string;
+ onPlay: (track: any) => void;
+}
+
+export function PlayerQueue({ isOpen, onClose, currentTrackId, onPlay }: PlayerQueueProps) {
+ const { queue, currentIndex, removeFromQueue, clearQueue } = usePlayerStore();
+
+ if (!isOpen) return null;
+
+ return (
+
+
+ {/* Header */}
+
+
+
Play Queue
+
+ {queue.length} Tracks
+
+
+
+
+ Clear
+
+
+
+
+
+
+
+ {/* Content */}
+
+ {queue.length === 0 ? (
+
+
+
Your queue is empty.
+
Add tracks to keep the vibe going.
+
+ ) : (
+
+
+ {queue.map((track, index) => {
+ const isCurrent = index === currentIndex;
+ const isPast = index < currentIndex;
+
+ return (
+
+ {/* Drag Handle (Simulated) */}
+
+
+
+
+ {/* Number/Status */}
+
+ {isCurrent ? (
+
+ ) : (
+ index + 1
+ )}
+
+
+ {/* Info */}
+
!isCurrent && onPlay(track)}
+ >
+
+ {track.title}
+
+
+ {track.artist}
+
+
+
+ {/* Actions */}
+
{
+ e.stopPropagation();
+ removeFromQueue(index);
+ }}
+ className="opacity-0 group-hover:opacity-100 p-2 text-muted-foreground hover:text-red-400 hover:bg-red-400/10 rounded-full transition-all"
+ >
+
+
+
+ );
+ })}
+
+
+ )}
+
+
+
+ {/* Backdrop for explicit dismissal on mobile if needed */}
+
+
+ );
+}