Add interactive video playback controls and progress tracking
Update VideoModal component to include a seekable progress bar, volume slider, time display, and mute functionality. Adds event listeners for timeupdate, durationchange, and volumechange. Implements custom CSS for the volume slider. Replit-Commit-Author: Agent Replit-Commit-Session-Id: d7424866-83d1-4486-a212-ac12b4c7becf Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/d7424866-83d1-4486-a212-ac12b4c7becf/ogtPjnM
This commit is contained in:
parent
532651d911
commit
dd5d0b5ba1
@ -63,6 +63,9 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
const [showControls, setShowControls] = useState(true);
|
||||
const [controlsTimeout, setControlsTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [volume, setVolume] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
@ -245,7 +248,13 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
||||
|
||||
videoElement.addEventListener('play', () => setIsPlaying(true));
|
||||
videoElement.addEventListener('pause', () => setIsPlaying(false));
|
||||
videoElement.addEventListener('volumechange', () => setIsMuted(videoElement.muted));
|
||||
videoElement.addEventListener('volumechange', () => {
|
||||
setIsMuted(videoElement.muted);
|
||||
setVolume(videoElement.volume);
|
||||
});
|
||||
videoElement.addEventListener('timeupdate', () => setCurrentTime(videoElement.currentTime));
|
||||
videoElement.addEventListener('loadedmetadata', () => setDuration(videoElement.duration));
|
||||
videoElement.addEventListener('durationchange', () => setDuration(videoElement.duration));
|
||||
}
|
||||
|
||||
// Cleanup when modal closes
|
||||
@ -345,6 +354,31 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
||||
return `${window.location.origin}?video=${video.id}`;
|
||||
};
|
||||
|
||||
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (videoRef.current && duration > 0) {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const clickX = e.clientX - rect.left;
|
||||
const newTime = (clickX / rect.width) * duration;
|
||||
videoRef.current.currentTime = newTime;
|
||||
setCurrentTime(newTime);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newVolume = parseFloat(e.target.value);
|
||||
if (videoRef.current) {
|
||||
videoRef.current.volume = newVolume;
|
||||
setVolume(newVolume);
|
||||
setIsMuted(newVolume === 0);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (time: number): string => {
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.floor(time % 60);
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(getShareUrl());
|
||||
@ -434,7 +468,21 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
||||
|
||||
{/* Bottom Control Bar */}
|
||||
{showControls && (
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 via-black/60 to-transparent p-4">
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 via-black/70 to-transparent p-4">
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-3">
|
||||
<div
|
||||
className="w-full h-2 bg-white/20 rounded-full cursor-pointer hover:h-3 transition-all duration-200"
|
||||
onClick={handleProgressClick}
|
||||
data-testid="progress-bar"
|
||||
>
|
||||
<div
|
||||
className="h-full bg-bunny-blue rounded-full transition-all duration-200"
|
||||
style={{ width: `${duration > 0 ? (currentTime / duration) * 100 : 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Play/Pause Button */}
|
||||
<Button
|
||||
@ -448,15 +496,32 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
||||
</Button>
|
||||
|
||||
{/* Volume Control */}
|
||||
<Button
|
||||
onClick={toggleMute}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-white hover:text-bunny-blue transition-colors"
|
||||
data-testid="button-volume"
|
||||
>
|
||||
{isMuted ? <VolumeX className="w-5 h-5" /> : <Volume2 className="w-5 h-5" />}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={toggleMute}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-white hover:text-bunny-blue transition-colors"
|
||||
data-testid="button-volume"
|
||||
>
|
||||
{isMuted ? <VolumeX className="w-5 h-5" /> : <Volume2 className="w-5 h-5" />}
|
||||
</Button>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={isMuted ? 0 : volume}
|
||||
onChange={handleVolumeChange}
|
||||
className="w-16 h-1 bg-white/20 rounded-lg appearance-none cursor-pointer slider"
|
||||
data-testid="volume-slider"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Time Display */}
|
||||
<div className="text-white text-sm font-mono" data-testid="time-display">
|
||||
{formatTime(currentTime)} / {formatTime(duration)}
|
||||
</div>
|
||||
|
||||
{/* Fullscreen Button */}
|
||||
<Button
|
||||
|
||||
@ -93,4 +93,43 @@
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Video player volume slider styles */
|
||||
.slider {
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-track {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 50%;
|
||||
background: hsl(217, 91%, 60%);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.slider::-moz-range-track {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 50%;
|
||||
background: hsl(217, 91%, 60%);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user