import React, { useState, useRef, useEffect } from 'react' import { useNexusStore } from '../store/nexusStore' import type { Track } from '../types/audio' interface AudioControls { loadTrack: (track: Track) => Promise play: () => Promise pause: () => void seekTo: (time: number) => void setVolume: (volume: number) => void getAudioElement?: () => HTMLAudioElement | null getCurrentTime?: () => number testAudioPlayback?: (url: string) => Promise } interface ControlPlinthProps { tracks: Track[] audioControls: AudioControls } const ControlPlinth: React.FC = ({ tracks, audioControls }) => { const [isTestingAudio, setIsTestingAudio] = useState(false) const [isDockCollapsed, setIsDockCollapsed] = useState(false) const timelineRef = useRef(null) const { playback, autoPlayEnabled, setIsPlaying, setCurrentTrack, setVolume, setCurrentTime, setAutoPlayEnabled, getNextTrack, getPreviousTrack } = useNexusStore() // Load initial track (auto-start is now handled by LivingNexusApp) useEffect(() => { const loadInitialTrack = async () => { if (tracks.length > 0 && !playback.currentTrack) { try { console.log('Loading initial track for controls:', tracks[0].title) // Test basic audio playback first if (audioControls.testAudioPlayback) { setIsTestingAudio(true) console.log('๐Ÿงช Running audio test...') const canPlay = await audioControls.testAudioPlayback(tracks[0].url) setIsTestingAudio(false) if (!canPlay) { console.error('โŒ Basic audio test failed - audio may not work') return } console.log('โœ… Basic audio test passed') } setCurrentTrack(tracks[0]) await audioControls.loadTrack(tracks[0]) console.log('Initial track loaded successfully (auto-start handled by main app)') } catch (error) { console.error('Failed to load initial track:', error) setIsTestingAudio(false) } } } loadInitialTrack() }, [tracks, playback.currentTrack, setCurrentTrack, audioControls, setIsTestingAudio]) const handlePlayPause = async () => { if (!playback.currentTrack) { console.log('No track selected, cannot play') return } if (isTestingAudio) { console.log('Audio test in progress, please wait...') return } try { if (playback.isPlaying) { audioControls.pause() setIsPlaying(false) } else { // Ensure track is loaded before playing console.log('Starting playback for:', playback.currentTrack.title) await audioControls.play() setIsPlaying(true) } } catch (error) { console.error('Playback control failed:', error) setIsPlaying(false) } } const handleTrackSelect = async (track: Track) => { console.log('Selecting track:', track.title) try { // Pause current track if playing if (playback.isPlaying) { audioControls.pause() setIsPlaying(false) } // Update UI immediately setCurrentTrack(track) // Load the new track console.log('Loading track:', track.title) await audioControls.loadTrack(track) console.log('Track loaded, starting playback') // Start playback await audioControls.play() setIsPlaying(true) } catch (error) { console.error('Track selection failed:', error) setIsPlaying(false) } } const handlePrevious = async () => { const previousTrack = getPreviousTrack() if (previousTrack) { await handleTrackSelect(previousTrack) } } const handleNext = async () => { const nextTrack = getNextTrack() if (nextTrack) { await handleTrackSelect(nextTrack) } } const handleTimelineClick = (e: React.MouseEvent) => { if (!timelineRef.current || !playback.currentTrack) return const rect = timelineRef.current.getBoundingClientRect() const clickX = e.clientX - rect.left const percentage = clickX / rect.width // Get actual duration from audio controls const audioElement = audioControls.getAudioElement?.() const duration = audioElement?.duration || 0 if (duration > 0) { const newTime = percentage * duration audioControls.seekTo(newTime) setCurrentTime(newTime) } } const handleVolumeChange = (e: React.ChangeEvent) => { const volume = parseFloat(e.target.value) setVolume(volume) audioControls.setVolume(volume) } const formatTime = (seconds: number): string => { const mins = Math.floor(seconds / 60) const secs = Math.floor(seconds % 60) return `${mins}:${secs.toString().padStart(2, '0')}` } const getPlayPauseGlyph = () => { return playback.isPlaying ? 'โ€–' : 'โ–ท' } const getCurrentProgress = (): number => { // Get actual duration and current time const audioElement = audioControls.getAudioElement?.() const duration = audioElement?.duration || 0 const currentTime = audioControls.getCurrentTime?.() || playback.currentTime return duration > 0 ? (currentTime / duration) * 100 : 0 } const getCurrentDuration = (): number => { const audioElement = audioControls.getAudioElement?.() return audioElement?.duration || 0 } return (
{ // Disable camera controls when mouse enters audio dock const event = new CustomEvent('disableCameraControls', { detail: true }) window.dispatchEvent(event) }} onMouseLeave={() => { // Re-enable camera controls when mouse leaves audio dock const event = new CustomEvent('disableCameraControls', { detail: false }) window.dispatchEvent(event) }} > {/* Dock Toggle Tab - Prominent and Always Visible */}
{/* Collapsed State - Enhanced with Progress */} {isDockCollapsed && (
{/* Vertical Progress Indicator */}
{/* Volume Indicator */}
๐Ÿ”Š
{playback.currentTrack?.title || 'No Track'}
{/* Track Position Indicator with Auto-play Status */} {playback.currentTrack && (
{tracks.findIndex(t => t.id === playback.currentTrack?.id) + 1}/{tracks.length}
{autoPlayEnabled && (
AUTO
)}
)}
)} {/* Expanded State - Full Controls */} {!isDockCollapsed && (
{/* Track Information Display */}
{playback.currentTrack?.title || 'Select Track'}
{playback.currentTrack?.artist || ''}
{/* Main Controls */}
{/* Transport Controls */}
{/* Timeline Scrubber */}
{/* Time Display */}
{formatTime(audioControls.getCurrentTime?.() || playback.currentTime)} {formatTime(getCurrentDuration())}
{/* Volume Control */}
{/* Auto-play Control */}
{/* Track List - Extended Vertically */}

Album Tracks

{tracks.map((track, index) => (
handleTrackSelect(track)} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '10px 8px', borderRadius: '6px', cursor: 'pointer', transition: 'all 0.2s ease', border: '1px solid transparent', background: playback.currentTrack?.id === track.id ? 'linear-gradient(45deg, rgba(255, 215, 0, 0.2), rgba(139, 105, 20, 0.3))' : 'transparent', borderColor: playback.currentTrack?.id === track.id ? 'rgba(255, 215, 0, 0.4)' : 'transparent' }} onMouseEnter={(e) => { if (playback.currentTrack?.id !== track.id) { e.currentTarget.style.background = 'rgba(255, 215, 0, 0.1)' e.currentTarget.style.borderColor = 'rgba(255, 215, 0, 0.2)' } }} onMouseLeave={(e) => { if (playback.currentTrack?.id !== track.id) { e.currentTarget.style.background = 'transparent' e.currentTarget.style.borderColor = 'transparent' } }} > {playback.currentTrack?.id === track.id && playback.isPlaying ? 'โ—ˆ' : (index + 1)}
{track.title}
{track.artist}
))}
)}
) } export default ControlPlinth