JavaScript 56.7%
TypeScript 42.6%
1"use client";23import { useState, useRef, useCallback, useEffect } from "react";4import {5 Play, Pause, Volume2, VolumeX, Maximize,6 SkipBack, SkipForward, Gauge7} from "lucide-react";8import type { PreviewPluginProps } from "./types";910export default function VideoPreview({ file }: PreviewPluginProps) {11 const videoRef = useRef<HTMLVideoElement>(null);12 const [isPlaying, setIsPlaying] = useState(false);13 const [currentTime, setCurrentTime] = useState(0);14 const [duration, setDuration] = useState(0);15 const [volume, setVolume] = useState(1);16 const [isMuted, setIsMuted] = useState(false);17 const [playbackRate, setPlaybackRate] = useState(1);18 const [showControls, setShowControls] = useState(true);19 const hideTimeout = useRef<ReturnType<typeof setTimeout>>();2021 const togglePlay = useCallback(() => {22 const video = videoRef.current;23 if (!video) return;24 if (video.paused) {25 video.play();26 setIsPlaying(true);27 } else {28 video.pause();29 setIsPlaying(false);30 }31 }, []);3233 const handleSeek = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {34 const video = videoRef.current;35 if (!video) return;36 const time = parseFloat(e.target.value);37 video.currentTime = time;38 setCurrentTime(time);39 }, []);4041 const skip = useCallback((seconds: number) => {42 const video = videoRef.current;43 if (!video) return;44 video.currentTime = Math.max(0, Math.min(video.currentTime + seconds, duration));45 }, [duration]);4647 const toggleMute = useCallback(() => {48 const video = videoRef.current;49 if (!video) return;50 video.muted = !video.muted;51 setIsMuted(video.muted);52 }, []);5354 const changeVolume = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {55 const video = videoRef.current;56 if (!video) return;57 const vol = parseFloat(e.target.value);58 video.volume = vol;59 setVolume(vol);60 setIsMuted(vol === 0);61 }, []);6263 const cycleSpeed = useCallback(() => {64 const speeds = [0.5, 1, 1.25, 1.5, 2];65 const idx = speeds.indexOf(playbackRate);66 const next = speeds[(idx + 1) % speeds.length];67 setPlaybackRate(next);68 if (videoRef.current) videoRef.current.playbackRate = next;69 }, [playbackRate]);7071 const toggleFullscreen = useCallback(() => {72 const video = videoRef.current;73 if (!video) return;74 if (document.fullscreenElement) {75 document.exitFullscreen();76 } else {77 video.requestFullscreen();78 }79 }, []);8081 const handleMouseMove = useCallback(() => {82 setShowControls(true);83 if (hideTimeout.current) clearTimeout(hideTimeout.current);84 hideTimeout.current = setTimeout(() => {85 if (isPlaying) setShowControls(false);86 }, 3000);87 }, [isPlaying]);8889 useEffect(() => {90 const handleKey = (e: KeyboardEvent) => {91 switch (e.key) {92 case " ": e.preventDefault(); togglePlay(); break;93 case "ArrowLeft": skip(-10); break;94 case "ArrowRight": skip(10); break;95 case "f": toggleFullscreen(); break;96 case "m": toggleMute(); break;97 }98 };99 window.addEventListener("keydown", handleKey);100 return () => window.removeEventListener("keydown", handleKey);101 }, [togglePlay, skip, toggleFullscreen, toggleMute]);102103 const formatTime = (t: number) => {104 const m = Math.floor(t / 60);105 const s = Math.floor(t % 60);106 return `${m}:${s.toString().padStart(2, "0")}`;107 };108109 return (110 <div111 className="flex flex-col h-full bg-black relative"112 onMouseMove={handleMouseMove}113 >114 <div className="flex-1 flex items-center justify-center relative">115 <video116 ref={videoRef}117 src={`/api/files/${file.id}/stream`}118 className="max-w-full max-h-full"119 onClick={togglePlay}120 onTimeUpdate={() => setCurrentTime(videoRef.current?.currentTime || 0)}121 onLoadedMetadata={() => setDuration(videoRef.current?.duration || 0)}122 onEnded={() => setIsPlaying(false)}123 preload="metadata"124 />125126 {/* Play overlay */}127 {!isPlaying && (128 <button129 onClick={togglePlay}130 className="absolute inset-0 flex items-center justify-center bg-black/30"131 >132 <div className="w-16 h-16 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center">133 <Play className="w-8 h-8 text-white ml-1" />134 </div>135 </button>136 )}137 </div>138139 {/* Controls */}140 <div141 className={`px-4 py-3 bg-gradient-to-t from-black/90 to-transparent transition-opacity ${142 showControls ? "opacity-100" : "opacity-0"143 }`}144 >145 {/* Seek bar */}146 <input147 type="range"148 min={0}149 max={duration || 0}150 step={0.1}151 value={currentTime}152 onChange={handleSeek}153 className="w-full h-1 mb-2 appearance-none bg-white/20 rounded-full cursor-pointer154 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3155 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white"156 />157158 <div className="flex items-center gap-3">159 <button onClick={() => skip(-10)} className="text-white/70 hover:text-white">160 <SkipBack className="w-4 h-4" />161 </button>162 <button onClick={togglePlay} className="text-white">163 {isPlaying ? <Pause className="w-5 h-5" /> : <Play className="w-5 h-5" />}164 </button>165 <button onClick={() => skip(10)} className="text-white/70 hover:text-white">166 <SkipForward className="w-4 h-4" />167 </button>168169 <span className="text-xs text-white/60 min-w-[5rem]">170 {formatTime(currentTime)} / {formatTime(duration)}171 </span>172173 <div className="flex-1" />174175 <button onClick={cycleSpeed} className="text-xs text-white/70 hover:text-white flex items-center gap-1">176 <Gauge className="w-3 h-3" />177 {playbackRate}x178 </button>179180 <div className="flex items-center gap-1">181 <button onClick={toggleMute} className="text-white/70 hover:text-white">182 {isMuted || volume === 0 ? <VolumeX className="w-4 h-4" /> : <Volume2 className="w-4 h-4" />}183 </button>184 <input185 type="range"186 min={0}187 max={1}188 step={0.05}189 value={isMuted ? 0 : volume}190 onChange={changeVolume}191 className="w-16 h-1 appearance-none bg-white/20 rounded-full cursor-pointer192 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-2.5 [&::-webkit-slider-thumb]:h-2.5193 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white"194 />195 </div>196197 <button onClick={toggleFullscreen} className="text-white/70 hover:text-white">198 <Maximize className="w-4 h-4" />199 </button>200 </div>201 </div>202 </div>203 );204}205