SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
4 h agolast push
JavaScript 56.7% TypeScript 42.6%
6.9 KB · 205 lines tsx
Raw Blame History
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