286 lines
8.7 KiB
TypeScript
286 lines
8.7 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
Drawer,
|
|
Upload,
|
|
Form,
|
|
Input,
|
|
Progress,
|
|
Alert,
|
|
Space,
|
|
Typography,
|
|
List,
|
|
Tag,
|
|
Button,
|
|
Grid,
|
|
} from 'antd';
|
|
import { InboxOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
|
import type { UploadFile } from 'antd/es/upload/interface';
|
|
import { mediaApi } from '@/lib/media-api';
|
|
|
|
const { Dragger } = Upload;
|
|
const { Text } = Typography;
|
|
|
|
interface UploadVideoDrawerProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSuccess: () => void;
|
|
}
|
|
|
|
interface UploadResult {
|
|
filename: string;
|
|
success: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
export default function UploadVideoDrawer({ open, onClose, onSuccess }: UploadVideoDrawerProps) {
|
|
const [form] = Form.useForm();
|
|
const screens = Grid.useBreakpoint();
|
|
const isMobile = !screens.md;
|
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
|
const [uploading, setUploading] = useState(false);
|
|
const [uploadProgress, setUploadProgress] = useState(0);
|
|
const [results, setResults] = useState<UploadResult[]>([]);
|
|
const [showResults, setShowResults] = useState(false);
|
|
|
|
const maxFileSize = 10 * 1024 * 1024 * 1024; // 10GB
|
|
|
|
const handleClose = () => {
|
|
if (!uploading) {
|
|
form.resetFields();
|
|
setFileList([]);
|
|
setResults([]);
|
|
setShowResults(false);
|
|
setUploadProgress(0);
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
const beforeUpload = (file: File) => {
|
|
// Validate file size
|
|
if (file.size > maxFileSize) {
|
|
return Upload.LIST_IGNORE;
|
|
}
|
|
|
|
// Validate file type
|
|
const validTypes = ['video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/x-matroska', 'video/webm', 'video/x-m4v', 'video/x-flv'];
|
|
const validExtensions = ['.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v', '.flv'];
|
|
const ext = file.name.toLowerCase().match(/\.[^.]+$/)?.[0];
|
|
|
|
if (!validTypes.includes(file.type) && !validExtensions.includes(ext || '')) {
|
|
return Upload.LIST_IGNORE;
|
|
}
|
|
|
|
return false; // Prevent auto-upload
|
|
};
|
|
|
|
const handleUpload = async () => {
|
|
if (fileList.length === 0) {
|
|
return;
|
|
}
|
|
|
|
setUploading(true);
|
|
setShowResults(false);
|
|
setResults([]);
|
|
setUploadProgress(0);
|
|
|
|
const uploadResults: UploadResult[] = [];
|
|
let completed = 0;
|
|
|
|
// Upload files sequentially
|
|
for (const file of fileList) {
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('file', file.originFileObj as File);
|
|
|
|
// Add metadata for single file uploads
|
|
if (fileList.length === 1) {
|
|
const values = form.getFieldsValue();
|
|
if (values.title) formData.append('title', values.title);
|
|
if (values.producer) formData.append('producer', values.producer);
|
|
if (values.creator) formData.append('creator', values.creator);
|
|
}
|
|
|
|
await mediaApi.post('/videos/upload', formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
},
|
|
onUploadProgress: (progressEvent) => {
|
|
if (progressEvent.total) {
|
|
const fileProgress = (progressEvent.loaded / progressEvent.total) * 100;
|
|
const totalProgress = ((completed + fileProgress / 100) / fileList.length) * 100;
|
|
setUploadProgress(Math.round(totalProgress));
|
|
}
|
|
},
|
|
});
|
|
|
|
uploadResults.push({
|
|
filename: file.name,
|
|
success: true,
|
|
});
|
|
} catch (error: any) {
|
|
uploadResults.push({
|
|
filename: file.name,
|
|
success: false,
|
|
error: error.response?.data?.message || 'Upload failed',
|
|
});
|
|
}
|
|
|
|
completed++;
|
|
setUploadProgress(Math.round((completed / fileList.length) * 100));
|
|
}
|
|
|
|
setResults(uploadResults);
|
|
setShowResults(true);
|
|
setUploading(false);
|
|
|
|
// Auto-close and refresh if all successful
|
|
const allSuccess = uploadResults.every((r) => r.success);
|
|
if (allSuccess) {
|
|
setTimeout(() => {
|
|
onSuccess();
|
|
handleClose();
|
|
}, 1500);
|
|
}
|
|
};
|
|
|
|
const successCount = results.filter((r) => r.success).length;
|
|
const failCount = results.length - successCount;
|
|
|
|
return (
|
|
<Drawer
|
|
title="Upload Videos"
|
|
open={open}
|
|
onClose={handleClose}
|
|
placement="right"
|
|
width={isMobile ? '100%' : 520}
|
|
mask={false}
|
|
destroyOnClose
|
|
closable={!uploading}
|
|
styles={{
|
|
wrapper: { top: 64, height: 'calc(100vh - 64px)' },
|
|
body: { padding: 24, overflowY: 'auto' },
|
|
}}
|
|
extra={
|
|
<Space>
|
|
<Button onClick={handleClose} disabled={uploading}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="primary"
|
|
onClick={handleUpload}
|
|
disabled={fileList.length === 0 || uploading}
|
|
loading={uploading}
|
|
>
|
|
{uploading ? 'Uploading...' : 'Upload'}
|
|
</Button>
|
|
</Space>
|
|
}
|
|
>
|
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
|
{!showResults && (
|
|
<>
|
|
<Dragger
|
|
multiple
|
|
fileList={fileList}
|
|
onChange={({ fileList: newFileList }) => setFileList(newFileList)}
|
|
beforeUpload={beforeUpload}
|
|
accept="video/*,.mp4,.mov,.avi,.mkv,.webm,.m4v,.flv"
|
|
disabled={uploading}
|
|
showUploadList={{ showRemoveIcon: !uploading }}
|
|
>
|
|
<p className="ant-upload-drag-icon">
|
|
<InboxOutlined />
|
|
</p>
|
|
<p className="ant-upload-text">Click or drag video files to this area to upload</p>
|
|
<p className="ant-upload-hint">
|
|
Support for single or bulk upload. Maximum file size: 10GB per file.
|
|
<br />
|
|
Accepted formats: MP4, MOV, AVI, MKV, WebM, M4V, FLV
|
|
</p>
|
|
</Dragger>
|
|
|
|
{fileList.length === 1 && (
|
|
<Form form={form} layout="vertical">
|
|
<Alert
|
|
message="Optional Metadata"
|
|
description="Add metadata for this video (optional)"
|
|
type="info"
|
|
showIcon
|
|
style={{ marginBottom: 16 }}
|
|
/>
|
|
<Form.Item label="Title" name="title">
|
|
<Input placeholder="Video title (defaults to filename)" />
|
|
</Form.Item>
|
|
<Form.Item label="Producer" name="producer">
|
|
<Input placeholder="Producer name" />
|
|
</Form.Item>
|
|
<Form.Item label="Creator" name="creator">
|
|
<Input placeholder="Creator name" />
|
|
</Form.Item>
|
|
</Form>
|
|
)}
|
|
|
|
{fileList.length > 1 && (
|
|
<Alert
|
|
message={`${fileList.length} files selected for batch upload`}
|
|
description="Metadata fields are not available for batch uploads. Titles will default to filenames."
|
|
type="info"
|
|
showIcon
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{uploading && (
|
|
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
|
<Text>
|
|
Uploading {fileList.length} file(s)... This may take a while for large files.
|
|
</Text>
|
|
<Progress percent={uploadProgress} status="active" />
|
|
</Space>
|
|
)}
|
|
|
|
{showResults && (
|
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
<Alert
|
|
message={`Upload Complete: ${successCount} succeeded, ${failCount} failed`}
|
|
type={failCount === 0 ? 'success' : 'warning'}
|
|
showIcon
|
|
/>
|
|
<List
|
|
size="small"
|
|
bordered
|
|
dataSource={results}
|
|
renderItem={(result) => (
|
|
<List.Item>
|
|
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
|
<Text ellipsis style={{ maxWidth: 340 }}>
|
|
{result.filename}
|
|
</Text>
|
|
{result.success ? (
|
|
<Tag icon={<CheckCircleOutlined />} color="success">
|
|
Success
|
|
</Tag>
|
|
) : (
|
|
<Tag icon={<CloseCircleOutlined />} color="error" title={result.error}>
|
|
Failed
|
|
</Tag>
|
|
)}
|
|
</Space>
|
|
{result.error && (
|
|
<div style={{ marginTop: 4 }}>
|
|
<Text type="danger" style={{ fontSize: 12 }}>
|
|
{result.error}
|
|
</Text>
|
|
</div>
|
|
)}
|
|
</List.Item>
|
|
)}
|
|
/>
|
|
</Space>
|
|
)}
|
|
</Space>
|
|
</Drawer>
|
|
);
|
|
}
|