vidrip/frontend/src/pages/VideoPlayerPage.tsx

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;