161 lines
5.0 KiB
TypeScript
161 lines
5.0 KiB
TypeScript
import React, { useCallback } from 'react'
|
|
import { useDropzone } from 'react-dropzone'
|
|
import { Box, Typography, Paper, LinearProgress, Alert } from '@mui/material'
|
|
import { CloudUpload, InsertDriveFile } from '@mui/icons-material'
|
|
import { useUploadStore } from '../stores/uploadStore'
|
|
import { uploadFile, getTaskInfo } from '../api/client'
|
|
import { useTaskStore } from '../stores/taskStore'
|
|
|
|
const ALLOWED_EXTENSIONS = ['.mp3', '.flac', '.wav', '.m4a', '.ogg', '.opus', '.aac', '.wma', '.aiff', '.alac', '.ac3']
|
|
|
|
export const UploadZone: React.FC = () => {
|
|
const {
|
|
file,
|
|
fileName,
|
|
fileSize,
|
|
isUploading,
|
|
uploadProgress,
|
|
error,
|
|
setFile,
|
|
setFileName,
|
|
setFileSize,
|
|
setIsUploading,
|
|
setUploadProgress,
|
|
setError,
|
|
setTaskId,
|
|
setHasVideo,
|
|
setHasAudio,
|
|
setHasSubtitle,
|
|
setAudioCodec,
|
|
} = useUploadStore()
|
|
|
|
const { setTaskId: setTaskIdStore } = useTaskStore()
|
|
|
|
const onDrop = useCallback(
|
|
async (acceptedFiles: File[]) => {
|
|
if (acceptedFiles.length === 0) return
|
|
|
|
const selectedFile = acceptedFiles[0]
|
|
const extension = '.' + selectedFile.name.split('.').pop()?.toLowerCase()
|
|
|
|
if (!ALLOWED_EXTENSIONS.includes(extension)) {
|
|
setError(`Unsupported file format. Allowed: ${ALLOWED_EXTENSIONS.join(', ')}`)
|
|
return
|
|
}
|
|
|
|
setFile(selectedFile)
|
|
setFileName(selectedFile.name)
|
|
setFileSize(selectedFile.size)
|
|
setError(null)
|
|
setIsUploading(true)
|
|
setUploadProgress(0)
|
|
|
|
try {
|
|
const response = await uploadFile(selectedFile)
|
|
const taskId = response.task_id
|
|
setTaskId(taskId)
|
|
setTaskIdStore(taskId)
|
|
setUploadProgress(100)
|
|
setIsUploading(false)
|
|
|
|
// Fetch stream info
|
|
try {
|
|
const info = await getTaskInfo(taskId)
|
|
setHasVideo(info.has_video)
|
|
setHasAudio(info.has_audio)
|
|
setHasSubtitle(info.has_subtitle)
|
|
setAudioCodec(info.audio_codec)
|
|
} catch (err) {
|
|
console.error('Failed to fetch stream info:', err)
|
|
// Don't block the upload flow if this fails; we'll just assume no video
|
|
}
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.detail || err.message || 'Upload failed')
|
|
setIsUploading(false)
|
|
setUploadProgress(0)
|
|
setFile(null)
|
|
setFileName('')
|
|
setFileSize(0)
|
|
}
|
|
},
|
|
[setFile, setFileName, setFileSize, setIsUploading, setUploadProgress, setError, setTaskId, setTaskIdStore]
|
|
)
|
|
|
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
onDrop,
|
|
accept: {
|
|
'audio/*': ALLOWED_EXTENSIONS,
|
|
},
|
|
multiple: false,
|
|
disabled: isUploading,
|
|
})
|
|
|
|
const formatFileSize = (bytes: number): string => {
|
|
if (bytes < 1024) return `${bytes} B`
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
}
|
|
|
|
return (
|
|
<Box>
|
|
<Paper
|
|
{...getRootProps()}
|
|
sx={{
|
|
p: 4,
|
|
border: '2px dashed',
|
|
borderColor: isDragActive ? 'primary.main' : 'grey.300',
|
|
borderRadius: 2,
|
|
backgroundColor: isDragActive ? 'action.hover' : 'background.paper',
|
|
cursor: isUploading ? 'default' : 'pointer',
|
|
transition: 'all 0.2s ease',
|
|
textAlign: 'center',
|
|
}}
|
|
>
|
|
<input {...getInputProps()} />
|
|
|
|
{file ? (
|
|
<Box>
|
|
<InsertDriveFile sx={{ fontSize: 48, color: 'primary.main', mb: 1 }} />
|
|
<Typography variant="h6">{fileName}</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{formatFileSize(fileSize)}
|
|
</Typography>
|
|
{isUploading && (
|
|
<Box sx={{ mt: 2, width: '100%' }}>
|
|
<LinearProgress variant="determinate" value={uploadProgress} />
|
|
<Typography variant="caption" color="text.secondary">
|
|
{uploadProgress}% uploaded
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
{!isUploading && (
|
|
<Typography variant="caption" color="success.main" sx={{ mt: 1, display: 'block' }}>
|
|
✅ Uploaded successfully
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
) : (
|
|
<Box>
|
|
<CloudUpload sx={{ fontSize: 64, color: 'primary.main', mb: 2 }} />
|
|
<Typography variant="h6" color="text.secondary">
|
|
{isDragActive ? 'Drop your audio file here' : 'Drag & drop your audio file here'}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
|
or click to browse
|
|
</Typography>
|
|
<Typography variant="caption" color="text.secondary" sx={{ mt: 2, display: 'block' }}>
|
|
Supported formats: {ALLOWED_EXTENSIONS.join(', ')}
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
</Paper>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mt: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
</Box>
|
|
)
|
|
}
|