Migrate all max-h-[XXvh] and h-[XXvh] to design system tokens: - max-h-[85vh] → max-h-layout-modal (9 modals) - max-h-[80vh] → max-h-layout-modal-sm (4 modals + notification bell) - max-h-[70vh] → max-h-layout-modal-xs (AddToPlaylistModal) - max-h-[90vh] → max-h-layout-modal-lg (CreatorModal) - max-h-[400px] → max-h-layout-list (QueuePanel, OfflineQueueManager) - max-h-[500px] → max-h-layout-drawer (APIPlaygroundView) - h-[60vh] → h-layout-lyrics (FullPlayer, TrackDetailPageHero/Skeleton) - max-w-[400px] → max-w-lg (FullPlayer) Zero arbitrary viewport heights remain in modals and panels. Co-authored-by: Cursor <cursoragent@cursor.com>
143 lines
5.3 KiB
TypeScript
143 lines
5.3 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Minimize2 } from 'lucide-react';
|
|
import { Button } from '../ui/button';
|
|
import { useAudio } from '../../context/AudioContext';
|
|
import { PlayerControls } from './PlayerControls';
|
|
import { LyricsPanel } from './LyricsPanel';
|
|
import { WaveformVisualizer } from '../ui/WaveformVisualizer';
|
|
|
|
interface FullPlayerProps {
|
|
onClose: () => void;
|
|
}
|
|
|
|
export const FullPlayer: React.FC<FullPlayerProps> = ({ onClose }) => {
|
|
const {
|
|
currentTrack,
|
|
currentTime,
|
|
duration,
|
|
progress,
|
|
seek,
|
|
visualizerSettings,
|
|
} = useAudio();
|
|
const [showLyrics, setShowLyrics] = useState(false);
|
|
|
|
if (!currentTrack) return null;
|
|
|
|
const formatTime = (seconds: number) => {
|
|
const mins = Math.floor(seconds / 60);
|
|
const secs = Math.floor(seconds % 60);
|
|
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
|
|
};
|
|
|
|
const handleSeek = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const p = Math.max(0, Math.min(100, (x / rect.width) * 100));
|
|
seek(p);
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[60] bg-background flex flex-col animate-fadeIn">
|
|
{/* Ambient Backdrop */}
|
|
<div className="absolute inset-0 z-0 overflow-hidden">
|
|
<div className="absolute inset-0 bg-black/60 z-10 backdrop-blur-3xl"></div>
|
|
<img
|
|
src={currentTrack.coverUrl}
|
|
className="w-full h-full object-cover opacity-50 scale-125 blur-3xl"
|
|
/>
|
|
</div>
|
|
|
|
{/* Header */}
|
|
<div className="relative z-20 flex justify-between items-center p-8">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={onClose}
|
|
className="text-white/50 hover:text-white"
|
|
>
|
|
<Minimize2 className="w-6 h-6" />
|
|
</Button>
|
|
<div className="flex gap-2">
|
|
<span className="px-4 py-1 bg-white/10 rounded-full text-xs font-bold text-white tracking-widest border border-white/20 backdrop-blur">
|
|
LOSSLESS
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<div className="relative z-20 flex-1 flex flex-col md:flex-row items-center justify-center gap-12 px-8 pb-8 max-w-7xl mx-auto w-full">
|
|
{/* Left: Artwork & Metadata */}
|
|
<div
|
|
className={`flex flex-col items-center md:items-start text-center md:text-left transition-all duration-[var(--duration-slow)] ${showLyrics ? 'md:w-1/3' : 'md:w-1/2'}`}
|
|
>
|
|
<div
|
|
className="aspect-square w-full max-w-lg rounded-2xl overflow-hidden shadow-2xl mb-8 border border-white/10 relative group cursor-pointer"
|
|
onClick={() => setShowLyrics(!showLyrics)}
|
|
>
|
|
<img
|
|
src={currentTrack.coverUrl}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
{/* Visualizer Overlay if enabled */}
|
|
{visualizerSettings.mode !== 'off' && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-black/20 backdrop-blur-[2px] opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<span className="text-white font-bold uppercase tracking-widest text-sm border border-white px-4 py-2 rounded-full">
|
|
{showLyrics ? 'Hide Lyrics' : 'Show Lyrics'}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<h1 className="text-4xl md:text-6xl font-display font-bold text-white mb-2 leading-tight">
|
|
{currentTrack.title}
|
|
</h1>
|
|
<h2 className="text-xl md:text-2xl text-muted-foreground font-medium mb-1">
|
|
{currentTrack.artist}
|
|
</h2>
|
|
<p className="text-white/50 font-mono text-sm">
|
|
{currentTrack.album} • {currentTrack.genre}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Right: Lyrics View */}
|
|
{showLyrics && (
|
|
<div className="flex-1 h-layout-lyrics w-full md:w-auto animate-slideInRight">
|
|
<LyricsPanel />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Controls & Waveform */}
|
|
<div className="relative z-20 p-8 pb-12 max-w-4xl mx-auto w-full">
|
|
<div className="flex items-center justify-between text-xs font-mono text-white/50 mb-2">
|
|
<span>{formatTime(currentTime)}</span>
|
|
<span>{formatTime(duration)}</span>
|
|
</div>
|
|
|
|
{/* Seek Bar / Waveform */}
|
|
<div className="h-16 mb-8 relative group" onClick={handleSeek}>
|
|
{visualizerSettings.mode === 'waveform' ? (
|
|
<WaveformVisualizer
|
|
progress={progress}
|
|
onSeek={seek}
|
|
height={64}
|
|
color="rgba(255,255,255,0.2)"
|
|
playedColor={visualizerSettings.color}
|
|
/>
|
|
) : (
|
|
<div className="h-2 bg-white/20 rounded-full cursor-pointer mt-7 relative">
|
|
<div
|
|
className="absolute top-0 left-0 h-full bg-white rounded-full transition-all"
|
|
style={{ width: `${progress}%` }}
|
|
>
|
|
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-4 h-4 bg-white rounded-full shadow-lg opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<PlayerControls layout="full" />
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|