From dd5d0b5ba1d88c171be01c86c94cc0952374e8c5 Mon Sep 17 00:00:00 2001 From: sebastjanartic <45803536-sebastjanartic@users.noreply.replit.com> Date: Thu, 7 Aug 2025 10:01:17 +0000 Subject: [PATCH] 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 --- client/src/components/video-modal.tsx | 87 +++++++++++++++++++++++---- client/src/index.css | 39 ++++++++++++ 2 files changed, 115 insertions(+), 11 deletions(-) diff --git a/client/src/components/video-modal.tsx b/client/src/components/video-modal.tsx index 5298bf9..4bf4150 100644 --- a/client/src/components/video-modal.tsx +++ b/client/src/components/video-modal.tsx @@ -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(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) => { + 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) => { + 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 && ( -
+
+ {/* Progress Bar */} +
+
+
0 ? (currentTime / duration) * 100 : 0}%` }} + /> +
+
+
{/* Play/Pause Button */} +
+ + +
+ + {/* Time Display */} +
+ {formatTime(currentTime)} / {formatTime(duration)} +
{/* Fullscreen Button */}