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 [isMuted, setIsMuted] = useState(false);
|
||||||
const [showControls, setShowControls] = useState(true);
|
const [showControls, setShowControls] = useState(true);
|
||||||
const [controlsTimeout, setControlsTimeout] = useState<NodeJS.Timeout | null>(null);
|
const [controlsTimeout, setControlsTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||||
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
|
const [duration, setDuration] = useState(0);
|
||||||
|
const [volume, setVolume] = useState(1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleEscape = (e: KeyboardEvent) => {
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
@ -245,7 +248,13 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
|||||||
|
|
||||||
videoElement.addEventListener('play', () => setIsPlaying(true));
|
videoElement.addEventListener('play', () => setIsPlaying(true));
|
||||||
videoElement.addEventListener('pause', () => setIsPlaying(false));
|
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
|
// Cleanup when modal closes
|
||||||
@ -345,6 +354,31 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
|||||||
return `${window.location.origin}?video=${video.id}`;
|
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 () => {
|
const copyToClipboard = async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(getShareUrl());
|
await navigator.clipboard.writeText(getShareUrl());
|
||||||
@ -434,7 +468,21 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
|||||||
|
|
||||||
{/* Bottom Control Bar */}
|
{/* Bottom Control Bar */}
|
||||||
{showControls && (
|
{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">
|
<div className="flex items-center gap-4">
|
||||||
{/* Play/Pause Button */}
|
{/* Play/Pause Button */}
|
||||||
<Button
|
<Button
|
||||||
@ -448,15 +496,32 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Volume Control */}
|
{/* Volume Control */}
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
onClick={toggleMute}
|
<Button
|
||||||
variant="ghost"
|
onClick={toggleMute}
|
||||||
size="icon"
|
variant="ghost"
|
||||||
className="text-white hover:text-bunny-blue transition-colors"
|
size="icon"
|
||||||
data-testid="button-volume"
|
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>
|
{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 */}
|
{/* Fullscreen Button */}
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -93,4 +93,43 @@
|
|||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
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