Spaces:
Running
Running
| import { useState, useRef, useCallback } from "react"; | |
| import GlassContainer from "./GlassContainer"; | |
| import GlassButton from "./GlassButton"; | |
| import { GLASS_EFFECTS } from "../constants"; | |
| import type { VideoUploadState } from "../types"; | |
| interface VideoUploadScreenProps { | |
| onVideoReady: (videoState: VideoUploadState) => void; | |
| onBack: () => void; | |
| } | |
| export default function VideoUploadScreen({ onVideoReady, onBack }: VideoUploadScreenProps) { | |
| const [dragActive, setDragActive] = useState(false); | |
| const [uploadError, setUploadError] = useState<string | null>(null); | |
| const [isProcessing, setIsProcessing] = useState(false); | |
| const fileInputRef = useRef<HTMLInputElement>(null); | |
| const handleFiles = useCallback(async (files: FileList | null) => { | |
| if (!files || files.length === 0) return; | |
| const file = files[0]; | |
| // Validate file type | |
| if (!file.type.startsWith('video/')) { | |
| setUploadError('Please select a valid video file.'); | |
| return; | |
| } | |
| // Validate file size (100MB limit) | |
| if (file.size > 1024 * 1024 * 1024) { | |
| setUploadError('Video file is too large. Please select a file under 1GB.'); | |
| return; | |
| } | |
| setIsProcessing(true); | |
| setUploadError(null); | |
| try { | |
| // Create video element | |
| const videoElement = document.createElement('video'); | |
| videoElement.muted = true; | |
| videoElement.controls = false; | |
| videoElement.preload = 'metadata'; | |
| // Create object URL for the video | |
| const videoUrl = URL.createObjectURL(file); | |
| videoElement.src = videoUrl; | |
| // Wait for video metadata to load | |
| await new Promise<void>((resolve, reject) => { | |
| videoElement.onloadedmetadata = () => resolve(); | |
| videoElement.onerror = () => reject(new Error('Failed to load video')); | |
| videoElement.load(); | |
| }); | |
| // Validate video duration (max 10 minutes) | |
| if (videoElement.duration > 600) { | |
| setUploadError('Video is too long. Please select a video under 10 minutes.'); | |
| URL.revokeObjectURL(videoUrl); | |
| return; | |
| } | |
| const videoState: VideoUploadState = { | |
| file, | |
| videoElement, | |
| isReady: true | |
| }; | |
| onVideoReady(videoState); | |
| } catch (error) { | |
| console.error('Error processing video:', error); | |
| setUploadError('Failed to process video file. Please try a different file.'); | |
| } finally { | |
| setIsProcessing(false); | |
| } | |
| }, [onVideoReady]); | |
| const handleDrag = useCallback((e: React.DragEvent) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| }, []); | |
| const handleDragIn = useCallback((e: React.DragEvent) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { | |
| setDragActive(true); | |
| } | |
| }, []); | |
| const handleDragOut = useCallback((e: React.DragEvent) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| setDragActive(false); | |
| }, []); | |
| const handleDrop = useCallback((e: React.DragEvent) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| setDragActive(false); | |
| if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { | |
| handleFiles(e.dataTransfer.files); | |
| } | |
| }, [handleFiles]); | |
| const handleFileSelect = useCallback(() => { | |
| fileInputRef.current?.click(); | |
| }, []); | |
| const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { | |
| handleFiles(e.target.files); | |
| }, [handleFiles]); | |
| return ( | |
| <div className="absolute inset-0 text-white flex items-center justify-center p-8"> | |
| <div className="max-w-2xl w-full space-y-8"> | |
| {/* Header */} | |
| <GlassContainer | |
| className="rounded-3xl shadow-2xl hover:scale-105 transition-transform duration-200" | |
| role="banner" | |
| > | |
| <div className="p-8 text-center"> | |
| <h1 className="text-4xl font-bold text-gray-100 mb-4">Upload Video</h1> | |
| <p className="text-lg text-gray-300 leading-relaxed"> | |
| Select a video file to analyze with FastVLM | |
| </p> | |
| </div> | |
| </GlassContainer> | |
| {/* Upload Area */} | |
| <GlassContainer | |
| className={`rounded-2xl shadow-2xl transition-all duration-200 border-2 border-dashed ${ | |
| dragActive | |
| ? 'border-blue-400 bg-blue-500/10 scale-105' | |
| : uploadError | |
| ? 'border-red-400 bg-red-500/10' | |
| : 'border-gray-500 hover:border-gray-400 hover:scale-105' | |
| }`} | |
| bgColor={ | |
| dragActive | |
| ? GLASS_EFFECTS.COLORS.BUTTON_BG | |
| : uploadError | |
| ? GLASS_EFFECTS.COLORS.ERROR_BG | |
| : GLASS_EFFECTS.COLORS.DEFAULT_BG | |
| } | |
| onDrag={handleDrag} | |
| onDragStart={handleDrag} | |
| onDragEnd={handleDrag} | |
| onDragOver={handleDrag} | |
| onDragEnter={handleDragIn} | |
| onDragLeave={handleDragOut} | |
| onDrop={handleDrop} | |
| > | |
| <div className="p-12 text-center"> | |
| {isProcessing ? ( | |
| <div className="space-y-4"> | |
| <div className="text-6xl">⏳</div> | |
| <h3 className="text-xl font-semibold text-gray-200">Processing video...</h3> | |
| <p className="text-gray-400">Please wait while we prepare your video</p> | |
| </div> | |
| ) : ( | |
| <div className="space-y-6"> | |
| <div className="text-6xl">{dragActive ? '📂' : '📁'}</div> | |
| <div> | |
| <h3 className="text-xl font-semibold text-gray-200 mb-2"> | |
| {dragActive ? 'Drop your video here' : 'Upload Video File'} | |
| </h3> | |
| <p className="text-gray-400 mb-4"> | |
| Drag and drop a video file or click to browse | |
| </p> | |
| <div className="text-sm text-gray-500 space-y-1"> | |
| <p>Supported formats: MP4, WebM, AVI, MOV</p> | |
| <p>Maximum size: 100MB | Maximum duration: 10 minutes</p> | |
| </div> | |
| </div> | |
| <GlassButton | |
| onClick={handleFileSelect} | |
| className="px-6 py-3 rounded-xl" | |
| disabled={isProcessing} | |
| > | |
| Choose File | |
| </GlassButton> | |
| </div> | |
| )} | |
| {uploadError && ( | |
| <div className="mt-6 p-4 rounded-xl bg-red-500/20 border border-red-500/30"> | |
| <p className="text-red-300 font-medium">{uploadError}</p> | |
| </div> | |
| )} | |
| </div> | |
| </GlassContainer> | |
| {/* Back Button */} | |
| <div className="flex justify-center"> | |
| <GlassButton | |
| onClick={onBack} | |
| className="px-6 py-3 rounded-xl" | |
| disabled={isProcessing} | |
| > | |
| ← Back to Options | |
| </GlassButton> | |
| </div> | |
| {/* Hidden file input */} | |
| <input | |
| ref={fileInputRef} | |
| type="file" | |
| accept="video/*" | |
| onChange={handleFileChange} | |
| className="hidden" | |
| /> | |
| </div> | |
| </div> | |
| ); | |
| } | |