252 lines
8.1 KiB
TypeScript

import React, { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import {
Box,
Button,
VStack,
HStack,
Text,
Progress,
Alert,
AlertIcon,
AlertTitle,
AlertDescription,
Icon,
Input,
Checkbox,
Select,
useToast,
} from '@chakra-ui/react';
import { FiUpload, FiMusic, FiCheck, FiX } from 'react-icons/fi';
interface UploadProgress {
fileName: string;
progress: number;
status: 'uploading' | 'success' | 'error';
error?: string;
}
interface MusicUploadProps {
onUploadComplete?: (files: any[]) => void;
}
export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) => {
const [uploadProgress, setUploadProgress] = useState<UploadProgress[]>([]);
const [isUploading, setIsUploading] = useState(false);
const [targetFolder, setTargetFolder] = useState<string>('uploads');
const [folders, setFolders] = useState<string[]>([]);
const [markForScan, setMarkForScan] = useState<boolean>(true);
const toast = useToast();
const onDrop = useCallback(async (acceptedFiles: File[]) => {
setIsUploading(true);
const newProgress: UploadProgress[] = acceptedFiles.map(file => ({
fileName: file.name,
progress: 0,
status: 'uploading',
}));
setUploadProgress(newProgress);
const results = [];
for (let i = 0; i < acceptedFiles.length; i++) {
const file = acceptedFiles[i];
try {
const formData = new FormData();
formData.append('file', file);
formData.append('targetFolder', targetFolder);
formData.append('markForScan', String(markForScan));
const response = await fetch('/api/music/upload', {
method: 'POST',
body: formData,
});
if (response.ok) {
const result = await response.json();
results.push(result.musicFile);
// Update progress
setUploadProgress(prev =>
prev.map((item, index) =>
index === i
? { ...item, progress: 100, status: 'success' as const }
: item
)
);
} else {
const error = await response.json();
throw new Error(error.error || 'Upload failed');
}
} catch (error) {
console.error(`Error uploading ${file.name}:`, error);
setUploadProgress(prev =>
prev.map((item, index) =>
index === i
? {
...item,
progress: 0,
status: 'error' as const,
error: error instanceof Error ? error.message : 'Upload failed'
}
: item
)
);
}
}
setIsUploading(false);
if (results.length > 0) {
toast({
title: 'Upload Complete',
description: `Successfully uploaded ${results.length} file(s)`,
status: 'success',
duration: 5000,
isClosable: true,
});
onUploadComplete?.(results);
}
}, [onUploadComplete, toast]);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'audio/*': ['.mp3', '.wav', '.flac', '.aac', '.m4a', '.ogg', '.wma', '.opus'],
},
maxSize: 100 * 1024 * 1024, // 100MB
multiple: true,
});
// Load folders for dropdown
React.useEffect(() => {
(async () => {
try {
const res = await fetch('/api/music/folders');
if (!res.ok) throw new Error('Failed to load folders');
const data = await res.json();
const items = Array.isArray(data.folders) ? data.folders : [];
setFolders(items);
if (items.length > 0 && targetFolder === 'uploads') {
// default target: root (empty) if available, else first item
const defaultChoice = items.find((f: string) => f === '') || items[0];
setTargetFolder(defaultChoice.replace(/^\/+/, ''));
}
} catch (e) {
// ignore, keep text input fallback
}
})();
}, []);
const resetUploads = () => {
setUploadProgress([]);
};
return (
<VStack spacing={4} align="stretch" w="full">
<HStack>
<Box flex={1}>
<Text color="gray.300" fontSize="sm" mb={1}>Target S3 folder</Text>
{folders.length > 0 ? (
<Select value={targetFolder} onChange={(e) => setTargetFolder(e.target.value)} bg="gray.800" borderColor="gray.700">
{folders.map((f) => (
<option key={f || 'ROOT'} value={f}>{f || '(root)'}</option>
))}
</Select>
) : (
<Input value={targetFolder} onChange={(e) => setTargetFolder(e.target.value)} placeholder="e.g. Prep/ToScan" bg="gray.800" borderColor="gray.700" />
)}
</Box>
<Checkbox isChecked={markForScan} onChange={(e) => setMarkForScan(e.target.checked)} colorScheme="blue" alignSelf="end">
Add to "To Be Scanned"
</Checkbox>
</HStack>
<Box
{...getRootProps()}
border="2px dashed"
borderColor={isDragActive ? 'blue.400' : 'gray.600'}
borderRadius="lg"
p={8}
textAlign="center"
cursor="pointer"
transition="all 0.2s"
_hover={{
borderColor: 'blue.400',
bg: 'blue.900',
}}
bg={isDragActive ? 'blue.900' : 'gray.800'}
>
<input {...getInputProps()} />
<VStack spacing={3}>
<Icon as={isDragActive ? FiUpload : FiMusic} w={8} h={8} color="blue.400" />
<Text fontSize="lg" fontWeight="medium" color="white">
{isDragActive
? 'Drop the music files here...'
: 'Drag & drop music files here, or click to select'}
</Text>
<Text fontSize="sm" color="gray.400">
Supports MP3, WAV, FLAC, AAC, OGG, WMA, Opus (max 100MB per file)
</Text>
</VStack>
</Box>
{uploadProgress.length > 0 && (
<VStack spacing={3} align="stretch">
<HStack justify="space-between">
<Text fontWeight="medium" color="white">Upload Progress</Text>
<Button size="sm" variant="ghost" onClick={resetUploads} color="gray.400" _hover={{ bg: "gray.700" }}>
Clear
</Button>
</HStack>
{uploadProgress.map((item, index) => (
<Box key={index} p={3} border="1px" borderColor="gray.700" borderRadius="md" bg="gray.800">
<HStack justify="space-between" mb={2}>
<Text fontSize="sm" fontWeight="medium" noOfLines={1} color="white">
{item.fileName}
</Text>
<Icon
as={item.status === 'success' ? FiCheck : item.status === 'error' ? FiX : undefined}
color={item.status === 'success' ? 'green.400' : item.status === 'error' ? 'red.400' : 'gray.400'}
/>
</HStack>
{item.status === 'error' ? (
<Alert status="error" size="sm" bg="red.900" borderColor="red.700" color="red.100">
<AlertIcon color="red.300" />
<Box>
<AlertTitle color="red.100">Upload failed</AlertTitle>
<AlertDescription color="red.200">{item.error}</AlertDescription>
</Box>
</Alert>
) : (
<Progress
value={item.progress}
colorScheme={item.status === 'success' ? 'green' : 'blue'}
size="sm"
bg="gray.700"
/>
)}
</Box>
))}
</VStack>
)}
{isUploading && (
<Alert status="info" bg="blue.900" borderColor="blue.700" color="blue.100">
<AlertIcon color="blue.300" />
<Box>
<AlertTitle color="blue.100">Uploading files...</AlertTitle>
<AlertDescription color="blue.200">
Please wait while your music files are being uploaded and processed.
</AlertDescription>
</Box>
</Alert>
)}
</VStack>
);
};