diff options
| author | Hyteq <DerpsDer.es@gmail.com> | 2025-06-23 09:18:15 +0300 |
|---|---|---|
| committer | Hyteq <DerpsDer.es@gmail.com> | 2025-06-23 09:18:15 +0300 |
| commit | ed6ab6cd5b6fec5f8e7d7cbcaa5224393729164e (patch) | |
| tree | c9df67eeec551f40737b1b26ab7e992c8b55c3e2 /apps/web/src/components/ui | |
| parent | 589f4a20a170c221203f7515622c38900e2efde4 (diff) | |
| download | opencut-ed6ab6cd5b6fec5f8e7d7cbcaa5224393729164e.tar.gz | |
feat: cleaned up video player, added functionality, zooming in/out, timeline position viewer, keyboard controls
Diffstat (limited to 'apps/web/src/components/ui')
| -rw-r--r-- | apps/web/src/components/ui/video-player.tsx | 117 |
1 files changed, 117 insertions, 0 deletions
diff --git a/apps/web/src/components/ui/video-player.tsx b/apps/web/src/components/ui/video-player.tsx new file mode 100644 index 00000000..127cc5db --- /dev/null +++ b/apps/web/src/components/ui/video-player.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { useRef, useEffect } from "react"; +import { Button } from "./button"; +import { Play, Pause, Volume2 } from "lucide-react"; +import { usePlaybackStore } from "@/stores/playback-store"; + +interface VideoPlayerProps { + src: string; + poster?: string; + className?: string; +} + +export function VideoPlayer({ src, poster, className = "" }: VideoPlayerProps) { + const videoRef = useRef<HTMLVideoElement>(null); + const { isPlaying, currentTime, volume, play, pause, setVolume, setDuration, setCurrentTime } = usePlaybackStore(); + + useEffect(() => { + const video = videoRef.current; + if (!video) return; + + const handleTimeUpdate = () => { + setCurrentTime(video.currentTime); + }; + + const handleLoadedMetadata = () => { + setDuration(video.duration); + }; + + const handleSeekEvent = (e: CustomEvent) => { + video.currentTime = e.detail.time; + }; + + video.addEventListener("timeupdate", handleTimeUpdate); + video.addEventListener("loadedmetadata", handleLoadedMetadata); + window.addEventListener("playback-seek", handleSeekEvent as EventListener); + + return () => { + video.removeEventListener("timeupdate", handleTimeUpdate); + video.removeEventListener("loadedmetadata", handleLoadedMetadata); + window.removeEventListener("playback-seek", handleSeekEvent as EventListener); + }; + }, [setCurrentTime, setDuration]); + + useEffect(() => { + const video = videoRef.current; + if (!video) return; + + if (isPlaying) { + video.play().catch(console.error); + } else { + video.pause(); + } + }, [isPlaying]); + + useEffect(() => { + const video = videoRef.current; + if (!video) return; + video.volume = volume; + }, [volume]); + + const handleSeek = (e: React.MouseEvent<HTMLVideoElement>) => { + const video = videoRef.current; + if (!video) return; + + const rect = video.getBoundingClientRect(); + const x = e.clientX - rect.left; + const percentage = x / rect.width; + const newTime = percentage * video.duration; + + video.currentTime = newTime; + setCurrentTime(newTime); + }; + + return ( + <div className={`relative group ${className}`}> + <video + ref={videoRef} + src={src} + poster={poster} + className="w-full h-full object-cover cursor-pointer" + onClick={handleSeek} + playsInline + preload="metadata" + /> + + <div className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" /> + + <div className="absolute bottom-2 left-2 right-2 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity"> + <Button + variant="ghost" + size="icon" + className="h-8 w-8 bg-black/50 text-white hover:bg-black/70" + onClick={isPlaying ? pause : play} + > + {isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />} + </Button> + + <div className="flex-1 h-1 bg-white/30 rounded-full overflow-hidden"> + <div + className="h-full bg-white transition-all duration-100" + style={{ width: `${(currentTime / usePlaybackStore.getState().duration) * 100}%` }} + /> + </div> + + <Button + variant="ghost" + size="icon" + className="h-8 w-8 bg-black/50 text-white hover:bg-black/70" + onClick={() => setVolume(volume > 0 ? 0 : 1)} + > + <Volume2 className="h-4 w-4" /> + </Button> + </div> + </div> + ); +}
\ No newline at end of file |
