import { useState, useCallback } from 'react'; import { useDropzone } from 'react-dropzone'; import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Box, Typography, LinearProgress, List, ListItem, ListItemText, IconButton, Alert, } from '@mui/material'; import { CloudUpload as UploadIcon, Close as CloseIcon, CheckCircle as SuccessIcon, Error as ErrorIcon, } from '@mui/icons-material'; import api from '../services/api'; interface UploadFile { file: File; progress: number; status: 'pending' | 'uploading' | 'success' | 'error'; error?: string; assetId?: string; } interface UploadDialogProps { open: boolean; onClose: () => void; onComplete?: () => void; } export default function UploadDialog({ open, onClose, onComplete }: UploadDialogProps) { const [files, setFiles] = useState([]); const [uploading, setUploading] = useState(false); const onDrop = useCallback((acceptedFiles: File[]) => { const newFiles: UploadFile[] = acceptedFiles.map((file) => ({ file, progress: 0, status: 'pending', })); setFiles((prev) => [...prev, ...newFiles]); }, []); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.gif', '.webp'], 'video/*': ['.mp4', '.mov', '.avi', '.mkv', '.webm'], }, }); const updateFileProgress = (index: number, progress: number, status: UploadFile['status'], error?: string, assetId?: string) => { setFiles((prev) => prev.map((f, i) => i === index ? { ...f, progress, status, error, assetId } : f ) ); }; const uploadFile = async (file: File, index: number) => { try { updateFileProgress(index, 0, 'uploading'); // Step 1: Create upload const uploadData = await api.createUpload({ original_filename: file.name, content_type: file.type, size_bytes: file.size, }); updateFileProgress(index, 33, 'uploading', undefined, uploadData.asset_id); // Step 2: Upload file to backend await api.uploadFileToBackend(uploadData.asset_id, file); updateFileProgress(index, 66, 'uploading', undefined, uploadData.asset_id); // Step 3: Finalize upload await api.finalizeUpload(uploadData.asset_id); updateFileProgress(index, 100, 'success', undefined, uploadData.asset_id); } catch (error: any) { console.error('Upload failed:', error); updateFileProgress( index, 0, 'error', error.response?.data?.detail || 'Ошибка загрузки' ); } }; const handleUpload = async () => { setUploading(true); // Upload files in parallel (max 3 at a time) const batchSize = 3; for (let i = 0; i < files.length; i += batchSize) { const batch = files.slice(i, i + batchSize); await Promise.all( batch.map((file, batchIndex) => { const fileIndex = i + batchIndex; if (files[fileIndex].status === 'pending') { return uploadFile(files[fileIndex].file, fileIndex); } return Promise.resolve(); }) ); } setUploading(false); if (onComplete) { onComplete(); } }; const handleClose = () => { if (!uploading) { setFiles([]); onClose(); } }; const canUpload = files.length > 0 && files.some((f) => f.status === 'pending'); const allComplete = files.length > 0 && files.every((f) => f.status === 'success' || f.status === 'error'); return ( Загрузка файлов {files.length === 0 && ( {isDragActive ? 'Отпустите файлы для загрузки' : 'Перетащите файлы сюда'} или нажмите для выбора файлов Поддерживаются фото (JPG, PNG, GIF, WebP) и видео (MP4, MOV, AVI, MKV, WebM) )} {files.length > 0 && ( {files.map((uploadFile, index) => ( {uploadFile.status === 'success' && ( )} {uploadFile.status === 'error' && ( )} {uploadFile.status === 'uploading' && ( )} {uploadFile.error && ( {uploadFile.error} )} ))} )} {files.length > 0 && !allComplete && ( Добавить еще файлы )} {canUpload && ( )} ); }