import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Progress } from '@/components/ui/progress' import { Slider } from '@/components/ui/slider' import { filesService } from '@/lib/api/services/files' import { type MessageResponse, type PlayerMode, type PlayerState, playerService, } from '@/lib/api/services/player' import { soundsService } from '@/lib/api/services/sounds' import { PLAYER_EVENTS, playerEvents } from '@/lib/events' import { cn } from '@/lib/utils' import { ArrowRight, ArrowRightToLine, Download, ExternalLink, List, Maximize2, Minimize2, MoreVertical, Music, Pause, Play, Repeat, Repeat1, Shuffle, SkipBack, SkipForward, Square, Volume2, VolumeX, } from 'lucide-react' import { useCallback, useEffect, useState } from 'react' import { toast } from 'sonner' import { Playlist } from './Playlist' import { NumberFlowDuration } from '../ui/number-flow-duration' export type PlayerDisplayMode = 'normal' | 'minimized' | 'maximized' | 'sidebar' interface PlayerProps { className?: string onPlayerModeChange?: (mode: PlayerDisplayMode) => void } export function Player({ className, onPlayerModeChange }: PlayerProps) { const [state, setState] = useState({ status: 'stopped', mode: 'continuous', volume: 80, previous_volume: 80, position: 0, }) const [displayMode, setDisplayMode] = useState(() => { // Initialize from localStorage or default to 'normal' if (typeof window !== 'undefined') { const saved = localStorage.getItem( 'playerDisplayMode', ) as PlayerDisplayMode return saved && ['normal', 'minimized', 'maximized', 'sidebar'].includes(saved) ? saved : 'normal' } return 'normal' }) // Notify parent when display mode changes and save to localStorage useEffect(() => { onPlayerModeChange?.(displayMode) // Save to localStorage if (typeof window !== 'undefined') { localStorage.setItem('playerDisplayMode', displayMode) } }, [displayMode, onPlayerModeChange]) const [showPlaylist, setShowPlaylist] = useState(false) const [isLoading, setIsLoading] = useState(false) // Load initial state useEffect(() => { const loadState = async () => { try { const initialState = await playerService.getState() setState(initialState) } catch (error) { console.error('Failed to load player state:', error) } } loadState() }, []) // Listen for player state updates useEffect(() => { const handlePlayerState = (...args: unknown[]) => { const newState = args[0] as PlayerState setState(newState) } playerEvents.on(PLAYER_EVENTS.PLAYER_STATE, handlePlayerState) return () => { playerEvents.off(PLAYER_EVENTS.PLAYER_STATE, handlePlayerState) } }, []) // Handle body scroll when in fullscreen useEffect(() => { if (displayMode === 'maximized') { // Disable body scroll document.body.style.overflow = 'hidden' } else { // Re-enable body scroll document.body.style.overflow = 'unset' } // Cleanup when component unmounts return () => { document.body.style.overflow = 'unset' } }, [displayMode]) const executeAction = useCallback( async ( action: () => Promise, actionName: string, ) => { setIsLoading(true) try { await action() } catch (error) { console.error(`Failed to ${actionName}:`, error) toast.error(`Failed to ${actionName}`) } finally { setIsLoading(false) } }, [], ) const handlePlayPause = useCallback(() => { if (state.status === 'playing') { executeAction(playerService.pause, 'pause') } else { executeAction(playerService.play, 'play') } }, [state.status, executeAction]) const handleStop = useCallback(() => { executeAction(playerService.stop, 'stop') }, [executeAction]) const handlePrevious = useCallback(() => { executeAction(playerService.previous, 'go to previous track') }, [executeAction]) const handleNext = useCallback(() => { executeAction(playerService.next, 'go to next track') }, [executeAction]) const handleSeek = useCallback( (position: number[]) => { const newPosition = position[0] executeAction(() => playerService.seek(newPosition), 'seek') }, [executeAction], ) const handleVolumeChange = useCallback( (volume: number[]) => { const newVolume = volume[0] executeAction(() => playerService.setVolume(newVolume), 'change volume') }, [executeAction], ) const handleMute = useCallback(() => { if (state.volume === 0) { // Unmute executeAction(playerService.unmute, 'unmute') } else { // Mute executeAction(playerService.mute, 'mute') } }, [state.volume, executeAction]) const handleModeChange = useCallback(() => { const modes: PlayerMode[] = [ 'continuous', 'loop', 'loop_one', 'random', 'single', ] const currentIndex = modes.indexOf(state.mode) const nextMode = modes[(currentIndex + 1) % modes.length] executeAction(() => playerService.setMode(nextMode), 'change mode') }, [state.mode, executeAction]) const handleDownloadSound = useCallback(async () => { if (!state.current_sound) return try { await filesService.downloadSound(state.current_sound.id) toast.success('Download started') } catch (error) { console.error('Failed to download sound:', error) toast.error('Failed to download sound') } }, [state.current_sound]) const handleStopAllSounds = useCallback(async () => { try { await soundsService.stopSounds() toast.success('All sounds stopped') } catch (error) { console.error('Failed to stop all sounds:', error) toast.error('Failed to stop all sounds') } }, []) const getModeIcon = () => { switch (state.mode) { case 'continuous': return case 'loop': return case 'loop_one': return case 'random': return default: return } } const expandFromSidebar = useCallback(() => { setDisplayMode('normal') }, []) const getPlayerPosition = () => { switch (displayMode) { case 'minimized': return 'fixed bottom-4 right-4 z-50' case 'maximized': return 'fixed inset-0 z-50 bg-background' case 'sidebar': return 'hidden' // Hidden when in sidebar mode default: return 'fixed bottom-4 right-4 z-50' } } const renderMinimizedPlayer = () => (
) const renderNormalPlayer = () => ( {/* Window Controls */}
{/* Album Art / Thumbnail */}
{state.current_sound?.thumbnail ? (
{state.current_sound.name} { // Hide image and show music icon if thumbnail fails to load const target = e.target as HTMLImageElement target.style.display = 'none' const musicIcon = target.nextElementSibling as HTMLElement if (musicIcon) musicIcon.style.display = 'block' }} />
) : null}
{/* Track Info */}

{state.current_sound?.name || 'No track selected'}

{state.current_sound && (state.current_sound.extract_url || state.current_sound.id) && ( {state.current_sound.extract_url && ( Source )} File )}
{/* Progress Bar */}
{ const rect = e.currentTarget.getBoundingClientRect() const clickX = e.clientX - rect.left const percentage = clickX / rect.width const newPosition = Math.round(percentage * (state.duration || 0)) handleSeek([newPosition]) }} />
{/* Main Controls */}
{/* Secondary Controls */}
{state.mode.replace('_', ' ')}
{/* Playlist */} {showPlaylist && state.playlist && (
executeAction( () => playerService.playAtIndex(index), 'play track', ) } />
)}
) const renderMaximizedPlayer = () => (
{/* Header */}

Now Playing

{/* Main Player Area */}
{/* Large Album Art */}
{state.current_sound?.thumbnail ? ( {state.current_sound.name} { // Hide image and show music icon if thumbnail fails to load const target = e.target as HTMLImageElement target.style.display = 'none' const musicIcon = target.nextElementSibling as HTMLElement if (musicIcon) musicIcon.style.display = 'block' }} /> ) : null}
{/* Track Info */}

{state.current_sound?.name || 'No track selected'}

{state.current_sound && (state.current_sound.extract_url || state.current_sound.id) && ( {state.current_sound.extract_url && ( Source )} File )}
{/* Progress Bar */}
{ const rect = e.currentTarget.getBoundingClientRect() const clickX = e.clientX - rect.left const percentage = clickX / rect.width const newPosition = Math.round( percentage * (state.duration || 0), ) handleSeek([newPosition]) }} />
{/* Large Controls */}
{/* Secondary Controls */}
{state.mode.replace('_', ' ')}
{Math.round(state.volume)}%
{/* Playlist Sidebar */} {state.playlist && (

Playlist

{state.playlist.sounds.length} tracks

executeAction( () => playerService.playAtIndex(index), 'play track', ) } variant="maximized" />
)}
) // Expose expand function for external use useEffect(() => { // Store expand function globally so sidebar can access it const windowWithExpand = window as unknown as { __expandPlayerFromSidebar?: () => void } windowWithExpand.__expandPlayerFromSidebar = expandFromSidebar return () => { delete windowWithExpand.__expandPlayerFromSidebar } }, [expandFromSidebar]) if (!state) return null return (
{displayMode === 'minimized' && renderMinimizedPlayer()} {displayMode === 'normal' && renderNormalPlayer()} {displayMode === 'maximized' && renderMaximizedPlayer()}
) }