242 lines
8.9 KiB
TypeScript
242 lines
8.9 KiB
TypeScript
import React, { useRef, useState, useEffect } from 'react';
|
|
import { Play, Pause, RotateCcw, Volume2, Maximize, CheckCircle, ExternalLink } from 'lucide-react';
|
|
|
|
interface VideoPlayerProps {
|
|
videoUrl: string;
|
|
videoType: 'youtube' | 'direct';
|
|
lessonId: string;
|
|
onProgressUpdate: (percentage: number) => void;
|
|
initialProgress?: number;
|
|
}
|
|
|
|
export default function VideoPlayer({ videoUrl, videoType, lessonId, onProgressUpdate, initialProgress = 0 }: VideoPlayerProps) {
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const [currentTime, setCurrentTime] = useState(0);
|
|
const [duration, setDuration] = useState(0);
|
|
const [volume, setVolume] = useState(1);
|
|
const [isCompleted, setIsCompleted] = useState(initialProgress >= 90);
|
|
const [manualProgress, setManualProgress] = useState(initialProgress);
|
|
|
|
// Sync state when lesson changes
|
|
useEffect(() => {
|
|
setIsCompleted(initialProgress >= 90);
|
|
setManualProgress(initialProgress);
|
|
setIsPlaying(false);
|
|
setCurrentTime(0);
|
|
}, [lessonId, initialProgress]);
|
|
|
|
// For Direct HTML5 Video Player
|
|
const handleTimeUpdate = () => {
|
|
if (!videoRef.current) return;
|
|
const current = videoRef.current.currentTime;
|
|
const dur = videoRef.current.duration;
|
|
setCurrentTime(current);
|
|
|
|
if (dur > 0) {
|
|
const percentage = Math.round((current / dur) * 100);
|
|
setManualProgress(percentage);
|
|
if (percentage >= 90 && !isCompleted) {
|
|
setIsCompleted(true);
|
|
onProgressUpdate(100);
|
|
} else {
|
|
// Debounce progress updates or update periodically
|
|
onProgressUpdate(percentage);
|
|
}
|
|
}
|
|
};
|
|
|
|
const togglePlay = () => {
|
|
if (!videoRef.current) return;
|
|
if (isPlaying) {
|
|
videoRef.current.pause();
|
|
} else {
|
|
videoRef.current.play().catch(e => console.log('Autoplay blocked'));
|
|
}
|
|
setIsPlaying(!isPlaying);
|
|
};
|
|
|
|
const handleLoadedMetadata = () => {
|
|
if (!videoRef.current) return;
|
|
setDuration(videoRef.current.duration);
|
|
};
|
|
|
|
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const newVol = parseFloat(e.target.value);
|
|
setVolume(newVol);
|
|
if (videoRef.current) {
|
|
videoRef.current.volume = newVol;
|
|
}
|
|
};
|
|
|
|
const handleProgressSlider = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const pct = parseInt(e.target.value);
|
|
setManualProgress(pct);
|
|
onProgressUpdate(pct);
|
|
if (pct >= 90) {
|
|
setIsCompleted(true);
|
|
} else {
|
|
setIsCompleted(false);
|
|
}
|
|
};
|
|
|
|
const triggerManualCompletion = () => {
|
|
setIsCompleted(true);
|
|
setManualProgress(100);
|
|
onProgressUpdate(100);
|
|
};
|
|
|
|
const formatTime = (timeInSeconds: number) => {
|
|
const minutes = Math.floor(timeInSeconds / 60);
|
|
const seconds = Math.floor(timeInSeconds % 60);
|
|
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
|
|
};
|
|
|
|
// YouTube embed parser
|
|
const getYoutubeEmbedUrl = (url: string) => {
|
|
let videoId = '';
|
|
// Handle watch?v= format
|
|
if (url.includes('youtube.com/watch?v=')) {
|
|
videoId = url.split('v=')[1]?.split('&')[0] || '';
|
|
}
|
|
// Handle embed format
|
|
else if (url.includes('youtube.com/embed/')) {
|
|
videoId = url.split('embed/')[1]?.split('?')[0] || '';
|
|
}
|
|
// Handle share format youtu.be/
|
|
else if (url.includes('youtu.be/')) {
|
|
videoId = url.split('youtu.be/')[1]?.split('?')[0] || '';
|
|
} else {
|
|
videoId = url; // assume id was passed directly
|
|
}
|
|
|
|
return `https://www.youtube.com/embed/${videoId}?modestbranding=1&rel=0&showinfo=0&controls=1&iv_load_policy=3&disablekb=1&fs=1`;
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Player Frame Container */}
|
|
<div className="relative bg-black aspect-video rounded-xl overflow-hidden border border-white/10 shadow-2xl group/player">
|
|
{videoType === 'youtube' ? (
|
|
<iframe
|
|
key={lessonId}
|
|
src={getYoutubeEmbedUrl(videoUrl)}
|
|
title="YouTube Player"
|
|
className="w-full h-full border-0"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
allowFullScreen
|
|
/>
|
|
) : (
|
|
<div className="relative w-full h-full flex items-center justify-center">
|
|
<video
|
|
ref={videoRef}
|
|
src={videoUrl}
|
|
onTimeUpdate={handleTimeUpdate}
|
|
onLoadedMetadata={handleLoadedMetadata}
|
|
onClick={togglePlay}
|
|
className="w-full h-full object-contain"
|
|
/>
|
|
|
|
{/* Custom Direct Controls Overlay (Visible on Hover) */}
|
|
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/90 to-transparent p-4 flex flex-col space-y-2 opacity-0 group-hover/player:opacity-100 transition-opacity duration-300">
|
|
{/* Progress Bar */}
|
|
<div className="flex items-center space-x-2">
|
|
<input
|
|
type="range"
|
|
min={0}
|
|
max={duration || 100}
|
|
value={currentTime}
|
|
onChange={(e) => {
|
|
const time = parseFloat(e.target.value);
|
|
if (videoRef.current) videoRef.current.currentTime = time;
|
|
setCurrentTime(time);
|
|
}}
|
|
className="w-full accent-[#E50914] bg-zinc-800 h-1.5 rounded-full cursor-pointer appearance-none"
|
|
/>
|
|
</div>
|
|
|
|
{/* Bottom Controls */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<button onClick={togglePlay} className="text-white hover:text-[#E50914] transition-colors">
|
|
{isPlaying ? <Pause className="w-5 h-5 fill-white" /> : <Play className="w-5 h-5 fill-white" />}
|
|
</button>
|
|
<span className="text-xs text-zinc-300 font-mono">
|
|
{formatTime(currentTime)} / {formatTime(duration)}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-4">
|
|
<div className="flex items-center space-x-2">
|
|
<Volume2 className="w-4 h-4 text-zinc-400" />
|
|
<input
|
|
type="range"
|
|
min={0}
|
|
max={1}
|
|
step={0.1}
|
|
value={volume}
|
|
onChange={handleVolumeChange}
|
|
className="w-16 accent-white bg-zinc-850 h-1 rounded-full cursor-pointer"
|
|
/>
|
|
</div>
|
|
<button
|
|
onClick={() => videoRef.current?.requestFullscreen()}
|
|
className="text-white hover:text-[#E50914] transition-colors"
|
|
>
|
|
<Maximize className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Lesson Progress Status & Actions bar */}
|
|
<div className="glass-card p-4 rounded-xl flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
<div className="flex items-center space-x-3 w-full sm:w-auto">
|
|
<div className={`p-2 rounded-lg ${isCompleted ? 'bg-emerald-500/10 text-emerald-500' : 'bg-white/5 text-zinc-500'}`}>
|
|
<CheckCircle className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-bold text-white leading-tight">
|
|
{isCompleted ? 'Aula Concluída!' : 'Aula em Progresso'}
|
|
</p>
|
|
<p className="text-xs text-zinc-400 font-medium mt-0.5">
|
|
Progresso atual: <span className="text-white font-semibold">{manualProgress}%</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Manual Progress Slider for YouTube or easy debugging */}
|
|
<div className="flex items-center space-x-4 w-full sm:w-auto flex-1 max-w-xs">
|
|
<span className="text-[10px] text-zinc-500 font-bold uppercase whitespace-nowrap">Ajustar Progresso</span>
|
|
<input
|
|
type="range"
|
|
min={0}
|
|
max={100}
|
|
value={manualProgress}
|
|
onChange={handleProgressSlider}
|
|
className="w-full accent-[#E50914] bg-white/10 h-1.5 rounded-full cursor-pointer"
|
|
/>
|
|
</div>
|
|
|
|
<div className="w-full sm:w-auto flex justify-end">
|
|
{!isCompleted ? (
|
|
<button
|
|
onClick={triggerManualCompletion}
|
|
className="w-full sm:w-auto glass-btn-primary text-white text-xs font-bold px-4 py-2 rounded-lg flex items-center justify-center space-x-1.5 cursor-pointer"
|
|
>
|
|
<span>Concluir Aula</span>
|
|
</button>
|
|
) : (
|
|
<span className="text-xs text-emerald-500 font-bold flex items-center space-x-1">
|
|
<span>✓ Salvo na Área do Aluno</span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|