179 lines
6.3 KiB
TypeScript
179 lines
6.3 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { videosAPI } from '../services/api';
|
|
import { Video } from '../types';
|
|
|
|
function VideoPlayerPage() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const [video, setVideo] = useState<Video | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const [theaterMode, setTheaterMode] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (id) {
|
|
loadVideo(parseInt(id));
|
|
}
|
|
}, [id]);
|
|
|
|
const loadVideo = async (videoId: number) => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await videosAPI.getById(videoId);
|
|
|
|
if (data.status !== 'completed') {
|
|
setError('This video has not been downloaded yet');
|
|
return;
|
|
}
|
|
|
|
setVideo(data);
|
|
setError('');
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to load video');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const formatFileSize = (bytes: number | null) => {
|
|
if (!bytes) return 'N/A';
|
|
const mb = bytes / (1024 * 1024);
|
|
return `${mb.toFixed(2)} MB`;
|
|
};
|
|
|
|
const formatDuration = (seconds: number | null) => {
|
|
if (!seconds) return 'N/A';
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
const secs = Math.floor(seconds % 60);
|
|
|
|
if (hours > 0) {
|
|
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
}
|
|
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="px-4 sm:px-6 lg:px-8">
|
|
<div className="p-8 text-center text-gray-500 dark:text-gray-400">Loading...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !video) {
|
|
return (
|
|
<div className="px-4 sm:px-6 lg:px-8">
|
|
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded">
|
|
{error || 'Video not found'}
|
|
</div>
|
|
<button
|
|
onClick={() => navigate('/')}
|
|
className="mt-4 text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
|
|
>
|
|
← Back to videos
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={theaterMode ? '' : 'px-4 sm:px-6 lg:px-8'}>
|
|
{!theaterMode && (
|
|
<button
|
|
onClick={() => navigate('/')}
|
|
className="mb-4 text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 flex items-center gap-2"
|
|
>
|
|
← Back to videos
|
|
</button>
|
|
)}
|
|
|
|
<div className={`${theaterMode ? 'w-screen relative left-1/2 -translate-x-1/2' : ''} bg-white dark:bg-gray-800 shadow overflow-hidden ${
|
|
theaterMode ? '' : 'sm:rounded-lg'
|
|
}`}>
|
|
<div className={`bg-black relative ${
|
|
theaterMode ? '' : 'aspect-video'
|
|
}`}>
|
|
<video
|
|
controls
|
|
className="w-full h-full"
|
|
src={`/api/videos/${video.id}/stream`}
|
|
>
|
|
Your browser does not support the video tag.
|
|
</video>
|
|
|
|
{/* Theater Mode Toggle Button */}
|
|
<button
|
|
onClick={() => setTheaterMode(!theaterMode)}
|
|
className="absolute top-4 right-4 bg-black/70 hover:bg-black/90 text-white p-2 rounded-lg transition-colors z-10"
|
|
title={theaterMode ? 'Exit Theater Mode' : 'Enter Theater Mode'}
|
|
>
|
|
{theaterMode ? (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 5l7 7-7 7" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
|
|
{/* Exit Theater Mode Button (appears on hover in theater mode) */}
|
|
{theaterMode && (
|
|
<button
|
|
onClick={() => navigate('/')}
|
|
className="absolute top-4 left-4 bg-black/70 hover:bg-black/90 text-white px-4 py-2 rounded-lg transition-colors z-10 flex items-center gap-2"
|
|
>
|
|
← Back to videos
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{!theaterMode && (
|
|
<div className="p-6">
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">{video.title}</h1>
|
|
|
|
<div className="mt-4 grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-gray-500 dark:text-gray-400">Duration:</span>
|
|
<span className="ml-2 font-medium text-gray-900 dark:text-white">{formatDuration(video.duration)}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500 dark:text-gray-400">File Size:</span>
|
|
<span className="ml-2 font-medium text-gray-900 dark:text-white">{formatFileSize(video.fileSize)}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500 dark:text-gray-400">Downloaded:</span>
|
|
<span className="ml-2 font-medium text-gray-900 dark:text-white">
|
|
{video.downloadedAt
|
|
? new Date(video.downloadedAt).toLocaleString()
|
|
: 'N/A'}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500 dark:text-gray-400">Upload Date:</span>
|
|
<span className="ml-2 font-medium text-gray-900 dark:text-white">
|
|
{video.uploadDate
|
|
? new Date(
|
|
video.uploadDate.slice(0, 4) +
|
|
'-' +
|
|
video.uploadDate.slice(4, 6) +
|
|
'-' +
|
|
video.uploadDate.slice(6, 8)
|
|
).toLocaleDateString()
|
|
: 'N/A'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default VideoPlayerPage;
|