summaryrefslogtreecommitdiff
path: root/apps/web/src/components/ui/video-player.tsx
diff options
context:
space:
mode:
authorHyteq <DerpsDer.es@gmail.com>2025-06-23 09:18:15 +0300
committerHyteq <DerpsDer.es@gmail.com>2025-06-23 09:18:15 +0300
commited6ab6cd5b6fec5f8e7d7cbcaa5224393729164e (patch)
treec9df67eeec551f40737b1b26ab7e992c8b55c3e2 /apps/web/src/components/ui/video-player.tsx
parent589f4a20a170c221203f7515622c38900e2efde4 (diff)
downloadopencut-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/video-player.tsx')
-rw-r--r--apps/web/src/components/ui/video-player.tsx117
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