veza/apps/web/src/features/player/components/VolumeControl.tsx

179 lines
5.1 KiB
TypeScript
Raw Normal View History

/**
* Composant VolumeControl
* Contrôle du volume avec slider, bouton mute, affichage valeur et persistance
*/
import React, { useState, useRef, useCallback, useEffect } from 'react';
import { Volume2, VolumeX, Volume1 } from 'lucide-react';
import { cn } from '@/lib/utils';
export interface VolumeControlProps {
volume: number; // 0-100
muted: boolean;
onVolumeChange: (volume: number) => void;
onMuteToggle: () => void;
className?: string;
disabled?: boolean;
showValue?: boolean;
showSlider?: boolean;
}
export function VolumeControl({
volume,
muted,
onVolumeChange,
onMuteToggle,
className,
disabled = false,
showValue = false,
showSlider = true,
}: VolumeControlProps) {
const [isDragging, setIsDragging] = useState(false);
const sliderRef = useRef<HTMLDivElement>(null);
const displayVolume = muted ? 0 : volume;
const getTimeFromPosition = useCallback(
(clientX: number): number => {
if (!sliderRef.current) return volume;
const rect = sliderRef.current.getBoundingClientRect();
const x = clientX - rect.left;
const percentage = Math.max(0, Math.min(1, x / rect.width));
return Math.round(percentage * 100);
},
2025-12-13 02:34:34 +00:00
[volume],
);
const handleMouseDown = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (disabled) return;
setIsDragging(true);
const newVolume = getTimeFromPosition(e.clientX);
onVolumeChange(newVolume);
},
2025-12-13 02:34:34 +00:00
[disabled, getTimeFromPosition, onVolumeChange],
);
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!isDragging || disabled) return;
const newVolume = getTimeFromPosition(e.clientX);
onVolumeChange(newVolume);
},
2025-12-13 02:34:34 +00:00
[isDragging, disabled, getTimeFromPosition, onVolumeChange],
);
const handleMouseUp = useCallback(() => {
if (isDragging) {
setIsDragging(false);
}
}, [isDragging]);
useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}
return undefined;
}, [isDragging, handleMouseMove, handleMouseUp]);
const getVolumeIcon = () => {
if (muted || displayVolume === 0) {
return <VolumeX className="h-5 w-5" aria-hidden="true" />;
}
if (displayVolume < 50) {
return <Volume1 className="h-5 w-5" aria-hidden="true" />;
}
return <Volume2 className="h-5 w-5" aria-hidden="true" />;
};
const getVolumeLabel = () => {
if (muted) return 'Volume muet';
return `Volume: ${volume}%`;
};
return (
<div className={cn('flex items-center gap-2', className)}>
{/* Mute Button */}
<button
type="button"
onClick={onMuteToggle}
disabled={disabled}
className={cn(
'rounded-full flex items-center justify-center transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 h-10 w-10',
'bg-transparent text-foreground hover:bg-muted focus:ring-muted',
2025-12-13 02:34:34 +00:00
disabled && 'opacity-50 cursor-not-allowed',
)}
aria-label={getVolumeLabel()}
aria-pressed={muted}
aria-disabled={disabled}
title={getVolumeLabel()}
>
{getVolumeIcon()}
<span className="sr-only">{getVolumeLabel()}</span>
</button>
{/* Volume Slider */}
{showSlider && (
<div className="flex items-center gap-2 flex-1 min-w-0">
<div
ref={sliderRef}
className={cn(
'relative w-full h-2 group cursor-pointer',
2025-12-13 02:34:34 +00:00
disabled && 'cursor-not-allowed opacity-50',
)}
onMouseDown={handleMouseDown}
role="slider"
aria-label="Volume"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={displayVolume}
aria-disabled={disabled}
>
{/* Background track */}
<div className="absolute inset-0 bg-muted rounded-full" />
{/* Volume track */}
<div
className={cn(
'absolute left-0 top-0 h-full bg-primary rounded-full transition-all',
isDragging && 'bg-primary',
)}
style={{ width: `${displayVolume}%` }}
/>
{/* Thumb */}
<div
className={cn(
'absolute top-1/2 -translate-y-1/2 w-4 h-4 bg-primary rounded-full',
'opacity-0 group-hover:opacity-100 transition-opacity',
isDragging && 'opacity-100',
2025-12-13 02:34:34 +00:00
disabled && 'opacity-0',
)}
style={{ left: `calc(${displayVolume}% - 8px)` }}
/>
</div>
{/* Volume Value */}
{showValue && (
<span
className="text-xs text-muted-foreground min-w-8 text-right"
aria-label={`Volume: ${volume}%`}
>
{muted ? 'Mute' : `${volume}%`}
</span>
)}
</div>
)}
</div>
);
}
export default VolumeControl;