334 lines
11 KiB
TypeScript
334 lines
11 KiB
TypeScript
/**
|
|
* Composant AudioPlayer - Intégration complète
|
|
* Assemble tous les composants du player pour une expérience complète
|
|
*/
|
|
|
|
import { useRef, useEffect, useState } from 'react';
|
|
import { usePlayer } from '../hooks/usePlayer';
|
|
import { useStreamSync } from '../hooks/useStreamSync';
|
|
import { usePlayerStore } from '../store/playerStore';
|
|
import { useKeyboardShortcuts } from '../hooks/useKeyboardShortcuts';
|
|
import { TrackInfo } from './TrackInfo';
|
|
import { PlayPauseButton } from './PlayPauseButton';
|
|
import { NextPreviousButtons } from './NextPreviousButtons';
|
|
import { ProgressBar } from './ProgressBar';
|
|
import { TimeDisplay } from './TimeDisplay';
|
|
import { VolumeControl } from './VolumeControl';
|
|
import { RepeatShuffleButtons } from './RepeatShuffleButtons';
|
|
import { QualitySelector } from './QualitySelector';
|
|
import { PlaybackSpeedControl } from './PlaybackSpeedControl';
|
|
import { PlayerError } from './PlayerError';
|
|
import { PlayerLoading } from './PlayerLoading';
|
|
import { cn } from '@/lib/utils';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Wifi, WifiOff } from 'lucide-react';
|
|
import type { AudioQuality } from './QualitySelector';
|
|
import type { PlaybackSpeed } from './PlaybackSpeedControl';
|
|
|
|
export interface AudioPlayerProps {
|
|
className?: string;
|
|
autoPlay?: boolean;
|
|
preload?: 'none' | 'metadata' | 'auto';
|
|
showQualitySelector?: boolean;
|
|
showSpeedControl?: boolean;
|
|
compact?: boolean;
|
|
}
|
|
|
|
export function AudioPlayer({
|
|
className,
|
|
autoPlay = false,
|
|
preload = 'metadata',
|
|
showQualitySelector = true,
|
|
showSpeedControl = true,
|
|
compact = false,
|
|
}: AudioPlayerProps) {
|
|
const audioRef = useRef<HTMLAudioElement>(null);
|
|
const player = usePlayer(audioRef);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [quality, setQuality] = useState<AudioQuality>('auto');
|
|
const [playbackSpeed, setPlaybackSpeed] = useState<PlaybackSpeed>(1);
|
|
const currentTrack = usePlayerStore(state => state.currentTrack);
|
|
|
|
// Intégration de la synchronisation (Session ID mocké pour l'instant ou via props)
|
|
// Dans le futur, ça viendra du contexte de "Room" ou "Session"
|
|
// On utilise l'ID de la track comme session ID temporaire pour le dev
|
|
const sessionId = currentTrack?.id ? `session_${currentTrack.id}` : null;
|
|
|
|
const { isSynced } = useStreamSync({
|
|
sessionId,
|
|
trackId: currentTrack?.id ?? null,
|
|
});
|
|
|
|
// Activer les raccourcis clavier
|
|
useKeyboardShortcuts(player, {
|
|
enabled: true,
|
|
seekStep: 5,
|
|
volumeStep: 5,
|
|
});
|
|
|
|
// Gérer le lifecycle de l'élément audio
|
|
useEffect(() => {
|
|
const audio = audioRef.current;
|
|
if (!audio) return;
|
|
|
|
// Configuration initiale
|
|
audio.preload = preload;
|
|
audio.autoplay = autoPlay;
|
|
|
|
// Gérer les erreurs
|
|
const handleError = () => {
|
|
const error = new Error('Erreur de lecture audio');
|
|
setError(error);
|
|
setIsLoading(false);
|
|
};
|
|
|
|
// Gérer le chargement
|
|
const handleLoadStart = () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
};
|
|
|
|
const handleCanPlay = () => {
|
|
setIsLoading(false);
|
|
setError(null);
|
|
};
|
|
|
|
// Gérer la vitesse de lecture
|
|
const updatePlaybackRate = () => {
|
|
if (audio) {
|
|
audio.playbackRate = playbackSpeed;
|
|
}
|
|
};
|
|
|
|
audio.addEventListener('error', handleError);
|
|
audio.addEventListener('loadstart', handleLoadStart);
|
|
audio.addEventListener('canplay', handleCanPlay);
|
|
updatePlaybackRate();
|
|
|
|
return () => {
|
|
audio.removeEventListener('error', handleError);
|
|
audio.removeEventListener('loadstart', handleLoadStart);
|
|
audio.removeEventListener('canplay', handleCanPlay);
|
|
};
|
|
}, [preload, autoPlay, playbackSpeed]);
|
|
|
|
// Mettre à jour la vitesse de lecture quand elle change
|
|
useEffect(() => {
|
|
const audio = audioRef.current;
|
|
if (audio) {
|
|
audio.playbackRate = playbackSpeed;
|
|
}
|
|
}, [playbackSpeed]);
|
|
|
|
const handlePlayPause = () => {
|
|
if (player.isPlaying) {
|
|
player.pause();
|
|
} else {
|
|
player.resume();
|
|
}
|
|
};
|
|
|
|
const handleRetry = () => {
|
|
setError(null);
|
|
if (player.currentTrack) {
|
|
player.play(player.currentTrack);
|
|
}
|
|
};
|
|
|
|
const canGoNext = player.queue.length > 0 && player.currentIndex < player.queue.length - 1;
|
|
const canGoPrevious = player.queue.length > 0 && player.currentIndex > 0;
|
|
|
|
if (compact) {
|
|
return (
|
|
<div className={cn('flex items-center gap-4 p-4', className)} data-testid="audio-player">
|
|
<audio
|
|
ref={audioRef}
|
|
data-testid="audio-element"
|
|
style={{ display: 'none' }}
|
|
preload={preload}
|
|
autoPlay={autoPlay}
|
|
/>
|
|
|
|
{error && <PlayerError error={error} onRetry={handleRetry} />}
|
|
|
|
{isLoading && <PlayerLoading isLoading={isLoading} size="sm" />}
|
|
|
|
{player.currentTrack && !error && !isLoading && (
|
|
<>
|
|
<TrackInfo track={player.currentTrack} showCover={true} coverSize="sm" showMetadata={false} />
|
|
<PlayPauseButton isPlaying={player.isPlaying} onClick={handlePlayPause} size="sm" />
|
|
<NextPreviousButtons
|
|
onNext={player.next}
|
|
onPrevious={player.previous}
|
|
canGoNext={canGoNext}
|
|
canGoPrevious={canGoPrevious}
|
|
size="sm"
|
|
/>
|
|
<VolumeControl
|
|
volume={player.volume}
|
|
muted={player.muted}
|
|
onVolumeChange={player.setVolume}
|
|
onMuteToggle={player.toggleMute}
|
|
showValue={false}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{!player.currentTrack && !error && (
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">Aucune piste sélectionnée</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={cn('bg-white dark:bg-gray-900 rounded-lg shadow-lg p-6', className)} data-testid="audio-player">
|
|
{/* Élément audio HTML - invisible mais nécessaire pour la lecture */}
|
|
<audio
|
|
ref={audioRef}
|
|
data-testid="audio-element"
|
|
style={{ display: 'none' }}
|
|
preload={preload}
|
|
autoPlay={autoPlay}
|
|
/>
|
|
|
|
{/* Gestion des erreurs */}
|
|
{error && (
|
|
<div className="mb-4">
|
|
<PlayerError error={error} onRetry={handleRetry} />
|
|
</div>
|
|
)}
|
|
|
|
{/* État de chargement */}
|
|
{isLoading && (
|
|
<div className="mb-4">
|
|
<PlayerLoading isLoading={isLoading} message="Chargement de la piste..." />
|
|
</div>
|
|
)}
|
|
|
|
{/* Contenu principal */}
|
|
{player.currentTrack && !error && !isLoading && (
|
|
<div className="space-y-6">
|
|
{/* Informations de la piste */}
|
|
<div>
|
|
<TrackInfo
|
|
track={player.currentTrack}
|
|
showCover={true}
|
|
coverSize="lg"
|
|
showMetadata={true}
|
|
/>
|
|
</div>
|
|
|
|
{/* Barre de progression */}
|
|
<div className="space-y-2">
|
|
<ProgressBar
|
|
currentTime={player.currentTime}
|
|
duration={player.duration}
|
|
onSeek={player.seek}
|
|
showTooltip={true}
|
|
/>
|
|
<div className="flex items-center justify-between">
|
|
<TimeDisplay currentTime={player.currentTime} duration={player.duration} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Contrôles principaux */}
|
|
<div className="flex items-center justify-center gap-4">
|
|
<RepeatShuffleButtons
|
|
repeat={player.repeat}
|
|
shuffle={player.shuffle}
|
|
onRepeatChange={player.setRepeat}
|
|
onShuffleToggle={player.toggleShuffle}
|
|
size="md"
|
|
/>
|
|
|
|
<NextPreviousButtons
|
|
onNext={player.next}
|
|
onPrevious={player.previous}
|
|
canGoNext={canGoNext}
|
|
canGoPrevious={canGoPrevious}
|
|
size="md"
|
|
/>
|
|
|
|
<PlayPauseButton
|
|
isPlaying={player.isPlaying}
|
|
onClick={handlePlayPause}
|
|
size="lg"
|
|
variant="default"
|
|
/>
|
|
|
|
<NextPreviousButtons
|
|
onNext={player.next}
|
|
onPrevious={player.previous}
|
|
canGoNext={canGoNext}
|
|
canGoPrevious={canGoPrevious}
|
|
size="md"
|
|
/>
|
|
|
|
<VolumeControl
|
|
volume={player.volume}
|
|
muted={player.muted}
|
|
onVolumeChange={player.setVolume}
|
|
onMuteToggle={player.toggleMute}
|
|
showValue={true}
|
|
showSlider={true}
|
|
/>
|
|
</div>
|
|
|
|
{/* Contrôles avancés */}
|
|
{(showQualitySelector || showSpeedControl) && (
|
|
<div className="flex items-center justify-between gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
{/* Sync Status Indicator */}
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant={isSynced ? "success" : "default"} className={cn("gap-1", !isSynced && "text-gray-500")}>
|
|
{isSynced ? <Wifi className="h-3 w-3" /> : <WifiOff className="h-3 w-3" />}
|
|
<span className="text-xs">{isSynced ? 'Sync' : 'Local'}</span>
|
|
</Badge>
|
|
{sessionId && !isSynced && (
|
|
<span className="text-xs text-muted-foreground hidden lg:inline">Connecting to session...</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
{showSpeedControl && (
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-gray-600 dark:text-gray-400">Vitesse:</span>
|
|
<PlaybackSpeedControl
|
|
currentSpeed={playbackSpeed}
|
|
onSpeedChange={setPlaybackSpeed}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{showQualitySelector && (
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-gray-600 dark:text-gray-400">Qualité:</span>
|
|
<QualitySelector
|
|
currentQuality={quality}
|
|
onQualityChange={setQuality}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* État vide */}
|
|
{!player.currentTrack && !error && !isLoading && (
|
|
<div className="text-center py-12">
|
|
<div className="text-gray-500 dark:text-gray-400 mb-2">
|
|
Aucune piste sélectionnée
|
|
</div>
|
|
<div className="text-sm text-gray-400 dark:text-gray-500">
|
|
Sélectionnez une piste pour commencer la lecture
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default AudioPlayer;
|