252 lines
8.1 KiB
TypeScript
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>
|
|
);
|
|
};
|