feat: Update Music Storage page to use dark theme
- Update MusicStorage page with dark theme colors (gray.900, gray.800, gray.700) - Update MusicUpload component with dark theme styling - Update SongMatching component with dark theme colors - Match the color scheme of the main browser page - Use consistent text colors (white, gray.100, gray.400, gray.500) - Update cards, buttons, and alerts with dark backgrounds - Improve hover states and visual consistency The Music Storage page now has a consistent dark theme that matches the main Rekordbox Reader interface.
This commit is contained in:
parent
7f186c6337
commit
3317a69004
@ -123,7 +123,7 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
|
||||
<Box
|
||||
{...getRootProps()}
|
||||
border="2px dashed"
|
||||
borderColor={isDragActive ? 'blue.400' : 'gray.300'}
|
||||
borderColor={isDragActive ? 'blue.400' : 'gray.600'}
|
||||
borderRadius="lg"
|
||||
p={8}
|
||||
textAlign="center"
|
||||
@ -131,19 +131,19 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
|
||||
transition="all 0.2s"
|
||||
_hover={{
|
||||
borderColor: 'blue.400',
|
||||
bg: 'blue.50',
|
||||
bg: 'blue.900',
|
||||
}}
|
||||
bg={isDragActive ? 'blue.50' : 'transparent'}
|
||||
bg={isDragActive ? 'blue.900' : 'gray.800'}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<VStack spacing={3}>
|
||||
<Icon as={isDragActive ? FiUpload : FiMusic} w={8} h={8} color="blue.500" />
|
||||
<Text fontSize="lg" fontWeight="medium">
|
||||
<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.600">
|
||||
<Text fontSize="sm" color="gray.400">
|
||||
Supports MP3, WAV, FLAC, AAC, OGG, WMA, Opus (max 100MB per file)
|
||||
</Text>
|
||||
</VStack>
|
||||
@ -152,30 +152,30 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
|
||||
{uploadProgress.length > 0 && (
|
||||
<VStack spacing={3} align="stretch">
|
||||
<HStack justify="space-between">
|
||||
<Text fontWeight="medium">Upload Progress</Text>
|
||||
<Button size="sm" variant="ghost" onClick={resetUploads}>
|
||||
<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.200" borderRadius="md">
|
||||
<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}>
|
||||
<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.500' : item.status === 'error' ? 'red.500' : 'gray.400'}
|
||||
color={item.status === 'success' ? 'green.400' : item.status === 'error' ? 'red.400' : 'gray.400'}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
{item.status === 'error' ? (
|
||||
<Alert status="error" size="sm">
|
||||
<AlertIcon />
|
||||
<Alert status="error" size="sm" bg="red.900" borderColor="red.700" color="red.100">
|
||||
<AlertIcon color="red.300" />
|
||||
<Box>
|
||||
<AlertTitle>Upload failed</AlertTitle>
|
||||
<AlertDescription>{item.error}</AlertDescription>
|
||||
<AlertTitle color="red.100">Upload failed</AlertTitle>
|
||||
<AlertDescription color="red.200">{item.error}</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
) : (
|
||||
@ -183,6 +183,7 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
|
||||
value={item.progress}
|
||||
colorScheme={item.status === 'success' ? 'green' : 'blue'}
|
||||
size="sm"
|
||||
bg="gray.700"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
@ -191,11 +192,11 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
|
||||
)}
|
||||
|
||||
{isUploading && (
|
||||
<Alert status="info">
|
||||
<AlertIcon />
|
||||
<Alert status="info" bg="blue.900" borderColor="blue.700" color="blue.100">
|
||||
<AlertIcon color="blue.300" />
|
||||
<Box>
|
||||
<AlertTitle>Uploading files...</AlertTitle>
|
||||
<AlertDescription>
|
||||
<AlertTitle color="blue.100">Uploading files...</AlertTitle>
|
||||
<AlertDescription color="blue.200">
|
||||
Please wait while your music files are being uploaded and processed.
|
||||
</AlertDescription>
|
||||
</Box>
|
||||
|
||||
@ -31,6 +31,7 @@ import {
|
||||
StatHelpText,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Spinner,
|
||||
} from '@chakra-ui/react';
|
||||
import { FiPlay, FiLink, FiSearch, FiZap, FiMusic, FiCheck, FiX } from 'react-icons/fi';
|
||||
|
||||
@ -273,98 +274,96 @@ export const SongMatching: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box p={6} maxW="1200px" mx="auto">
|
||||
<VStack spacing={6} align="stretch">
|
||||
<Heading size="lg" textAlign="center">
|
||||
🎵 Song Matching & Linking
|
||||
</Heading>
|
||||
|
||||
{/* Statistics */}
|
||||
{stats && (
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
|
||||
<Card bg="gray.800" borderColor="gray.700" borderWidth="1px">
|
||||
<CardHeader>
|
||||
<Heading size="md" color="white">Matching Statistics</Heading>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4}>
|
||||
<Stat>
|
||||
<StatLabel>Total Songs</StatLabel>
|
||||
<StatNumber>{stats.totalSongs}</StatNumber>
|
||||
<StatHelpText>In Rekordbox library</StatHelpText>
|
||||
<StatLabel color="gray.400">Total Songs</StatLabel>
|
||||
<StatNumber color="white">{stats.totalSongs}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Music Files</StatLabel>
|
||||
<StatNumber>{stats.totalMusicFiles}</StatNumber>
|
||||
<StatHelpText>Uploaded to S3</StatHelpText>
|
||||
<StatLabel color="gray.400">Music Files</StatLabel>
|
||||
<StatNumber color="white">{stats.totalMusicFiles}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Match Rate</StatLabel>
|
||||
<StatNumber>{stats.matchRate}%</StatNumber>
|
||||
<StatHelpText>{stats.matchedMusicFiles} of {stats.totalMusicFiles} linked</StatHelpText>
|
||||
<StatLabel color="gray.400">Match Rate</StatLabel>
|
||||
<StatNumber color="green.400">{stats.matchRate}</StatNumber>
|
||||
<StatHelpText color="gray.500">
|
||||
{stats.matchedMusicFiles} of {stats.totalMusicFiles} files matched
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel color="gray.400">Unmatched</StatLabel>
|
||||
<StatNumber color="orange.400">{stats.unmatchedMusicFiles}</StatNumber>
|
||||
<StatHelpText color="gray.500">
|
||||
{stats.songsWithoutMusicFiles} songs without files
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
</SimpleGrid>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Auto-linking */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<HStack justify="space-between">
|
||||
<Heading size="md">Auto-Linking</Heading>
|
||||
{/* Auto-Link Button */}
|
||||
<Card bg="gray.800" borderColor="gray.700" borderWidth="1px">
|
||||
<CardBody>
|
||||
<VStack spacing={4}>
|
||||
<Text color="gray.300" textAlign="center">
|
||||
Automatically match and link music files to songs in your Rekordbox library
|
||||
</Text>
|
||||
<Button
|
||||
leftIcon={<FiZap />}
|
||||
colorScheme="blue"
|
||||
size="lg"
|
||||
onClick={handleAutoLink}
|
||||
isLoading={autoLinking}
|
||||
loadingText="Auto-linking..."
|
||||
loadingText="Auto-Linking..."
|
||||
_hover={{ bg: "blue.700" }}
|
||||
>
|
||||
Auto-Link Files
|
||||
</Button>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Text color="gray.600">
|
||||
Automatically match and link music files to songs in your Rekordbox library.
|
||||
This will attempt to find matches based on filename, title, artist, and other metadata.
|
||||
Original file paths are preserved and S3 information is added alongside.
|
||||
</Text>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* Unmatched Music Files */}
|
||||
<Card>
|
||||
<Card bg="gray.800" borderColor="gray.700" borderWidth="1px">
|
||||
<CardHeader>
|
||||
<Heading size="md">Unmatched Music Files ({unmatchedMusicFiles.length})</Heading>
|
||||
<Heading size="md" color="white">Unmatched Music Files ({unmatchedMusicFiles.length})</Heading>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{unmatchedMusicFiles.length === 0 ? (
|
||||
<Text color="gray.500" textAlign="center">
|
||||
All music files have been matched! 🎉
|
||||
All music files are matched! 🎉
|
||||
</Text>
|
||||
) : (
|
||||
<VStack spacing={3} align="stretch">
|
||||
{unmatchedMusicFiles.slice(0, 10).map((musicFile) => (
|
||||
{unmatchedMusicFiles.slice(0, 10).map((file) => (
|
||||
<Box
|
||||
key={musicFile._id}
|
||||
key={file._id}
|
||||
p={3}
|
||||
border="1px"
|
||||
borderColor="gray.200"
|
||||
borderColor="gray.700"
|
||||
borderRadius="md"
|
||||
bg="gray.900"
|
||||
>
|
||||
<HStack justify="space-between">
|
||||
<VStack align="start" spacing={1} flex={1}>
|
||||
<Text fontWeight="bold" fontSize="sm">
|
||||
{musicFile.title || musicFile.originalName}
|
||||
<Text fontWeight="bold" fontSize="sm" color="white">
|
||||
{file.title || file.originalName}
|
||||
</Text>
|
||||
{musicFile.artist && (
|
||||
<Text fontSize="xs" color="gray.600">
|
||||
{musicFile.artist}
|
||||
<Text fontSize="xs" color="gray.400">
|
||||
{file.artist}
|
||||
</Text>
|
||||
)}
|
||||
{musicFile.album && (
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{musicFile.album}
|
||||
{file.album}
|
||||
</Text>
|
||||
)}
|
||||
<HStack spacing={2} fontSize="xs" color="gray.500">
|
||||
<Text>{formatDuration(musicFile.duration || 0)}</Text>
|
||||
<Text>•</Text>
|
||||
<Text>{musicFile.format?.toUpperCase()}</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<HStack spacing={2}>
|
||||
<Tooltip label="Get matching suggestions">
|
||||
@ -373,7 +372,9 @@ export const SongMatching: React.FC = () => {
|
||||
icon={<FiSearch />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleGetSuggestions(musicFile)}
|
||||
colorScheme="blue"
|
||||
onClick={() => handleGetSuggestions(file)}
|
||||
_hover={{ bg: "blue.900" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label="Play music file">
|
||||
@ -382,7 +383,8 @@ export const SongMatching: React.FC = () => {
|
||||
icon={<FiPlay />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="blue"
|
||||
colorScheme="green"
|
||||
_hover={{ bg: "green.900" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
@ -400,62 +402,54 @@ export const SongMatching: React.FC = () => {
|
||||
</Card>
|
||||
|
||||
{/* Matched Music Files */}
|
||||
<Card>
|
||||
<Card bg="gray.800" borderColor="gray.700" borderWidth="1px">
|
||||
<CardHeader>
|
||||
<Heading size="md">Matched Music Files ({matchedMusicFiles.length})</Heading>
|
||||
<Heading size="md" color="white">Matched Music Files ({matchedMusicFiles.length})</Heading>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{matchedMusicFiles.length === 0 ? (
|
||||
<Text color="gray.500" textAlign="center">
|
||||
No music files have been matched yet.
|
||||
No music files are matched yet.
|
||||
</Text>
|
||||
) : (
|
||||
<VStack spacing={3} align="stretch">
|
||||
{matchedMusicFiles.slice(0, 10).map((musicFile) => (
|
||||
{matchedMusicFiles.slice(0, 10).map((file) => (
|
||||
<Box
|
||||
key={musicFile._id}
|
||||
key={file._id}
|
||||
p={3}
|
||||
border="1px"
|
||||
borderColor="green.200"
|
||||
borderColor="green.700"
|
||||
borderRadius="md"
|
||||
bg="green.50"
|
||||
bg="green.900"
|
||||
>
|
||||
<HStack justify="space-between">
|
||||
<VStack align="start" spacing={1} flex={1}>
|
||||
<HStack spacing={2}>
|
||||
<Text fontWeight="bold" fontSize="sm">
|
||||
{musicFile.title || musicFile.originalName}
|
||||
<Text fontWeight="bold" fontSize="sm" color="white">
|
||||
{file.title || file.originalName}
|
||||
</Text>
|
||||
<Badge colorScheme="green" size="sm">
|
||||
<Badge colorScheme="green" size="sm" bg="green.800" color="green.200">
|
||||
<FiCheck style={{ marginRight: '4px' }} />
|
||||
Linked
|
||||
Matched
|
||||
</Badge>
|
||||
</HStack>
|
||||
{musicFile.artist && (
|
||||
<Text fontSize="xs" color="gray.600">
|
||||
{musicFile.artist}
|
||||
<Text fontSize="xs" color="gray.300">
|
||||
{file.artist}
|
||||
</Text>
|
||||
)}
|
||||
{musicFile.songId && (
|
||||
<Text fontSize="xs" color="blue.600">
|
||||
→ {musicFile.songId.title} by {musicFile.songId.artist}
|
||||
<Text fontSize="xs" color="gray.400">
|
||||
{file.album}
|
||||
</Text>
|
||||
)}
|
||||
{musicFile.songId?.location && (
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
📁 {musicFile.songId.location}
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
<HStack spacing={2}>
|
||||
<Tooltip label="Unlink from song">
|
||||
<Tooltip label="Unlink music file">
|
||||
<IconButton
|
||||
aria-label="Unlink"
|
||||
icon={<FiX />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="red"
|
||||
onClick={() => handleUnlinkMusicFile(musicFile.songId._id)}
|
||||
onClick={() => handleUnlinkMusicFile(file.songId)}
|
||||
_hover={{ bg: "red.900" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label="Play music file">
|
||||
@ -465,6 +459,7 @@ export const SongMatching: React.FC = () => {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="blue"
|
||||
_hover={{ bg: "blue.900" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
@ -482,9 +477,9 @@ export const SongMatching: React.FC = () => {
|
||||
</Card>
|
||||
|
||||
{/* Songs with Music Files */}
|
||||
<Card>
|
||||
<Card bg="gray.800" borderColor="gray.700" borderWidth="1px">
|
||||
<CardHeader>
|
||||
<Heading size="md">Songs with Music Files ({songsWithMusicFiles.length})</Heading>
|
||||
<Heading size="md" color="white">Songs with Music Files ({songsWithMusicFiles.length})</Heading>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{songsWithMusicFiles.length === 0 ? (
|
||||
@ -498,31 +493,31 @@ export const SongMatching: React.FC = () => {
|
||||
key={song._id}
|
||||
p={3}
|
||||
border="1px"
|
||||
borderColor="blue.200"
|
||||
borderColor="blue.700"
|
||||
borderRadius="md"
|
||||
bg="blue.50"
|
||||
bg="blue.900"
|
||||
>
|
||||
<HStack justify="space-between">
|
||||
<VStack align="start" spacing={1} flex={1}>
|
||||
<HStack spacing={2}>
|
||||
<Text fontWeight="bold" fontSize="sm">
|
||||
<Text fontWeight="bold" fontSize="sm" color="white">
|
||||
{song.title}
|
||||
</Text>
|
||||
<Badge colorScheme="blue" size="sm">
|
||||
<Badge colorScheme="blue" size="sm" bg="blue.800" color="blue.200">
|
||||
<FiMusic style={{ marginRight: '4px' }} />
|
||||
Has S3 File
|
||||
</Badge>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color="gray.600">
|
||||
<Text fontSize="xs" color="gray.300">
|
||||
{song.artist}
|
||||
</Text>
|
||||
{song.location && (
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
<Text fontSize="xs" color="gray.400">
|
||||
📁 {song.location}
|
||||
</Text>
|
||||
)}
|
||||
{song.s3File?.streamingUrl && (
|
||||
<Text fontSize="xs" color="green.600">
|
||||
<Text fontSize="xs" color="green.400">
|
||||
🎵 S3: {song.s3File.s3Key}
|
||||
</Text>
|
||||
)}
|
||||
@ -536,6 +531,7 @@ export const SongMatching: React.FC = () => {
|
||||
variant="ghost"
|
||||
colorScheme="red"
|
||||
onClick={() => handleUnlinkMusicFile(song._id)}
|
||||
_hover={{ bg: "red.900" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label="Play music file">
|
||||
@ -545,6 +541,7 @@ export const SongMatching: React.FC = () => {
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="blue"
|
||||
_hover={{ bg: "blue.800" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
@ -564,63 +561,73 @@ export const SongMatching: React.FC = () => {
|
||||
{/* Suggestions Modal */}
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalContent bg="gray.800" borderColor="gray.700" borderWidth="1px">
|
||||
<ModalHeader color="white">
|
||||
Matching Suggestions for "{selectedMusicFile?.title || selectedMusicFile?.originalName}"
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalCloseButton color="gray.400" />
|
||||
<ModalBody>
|
||||
{loadingSuggestions ? (
|
||||
<Progress size="xs" isIndeterminate />
|
||||
<VStack spacing={4}>
|
||||
<Spinner size="lg" color="blue.400" />
|
||||
<Text color="gray.400">Finding matching songs...</Text>
|
||||
</VStack>
|
||||
) : suggestions.length === 0 ? (
|
||||
<Text color="gray.500" textAlign="center">
|
||||
No matching suggestions found.
|
||||
No matching songs found. You can manually link this file later.
|
||||
</Text>
|
||||
) : (
|
||||
<VStack spacing={3} align="stretch">
|
||||
{suggestions.map((match, index) => (
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
p={3}
|
||||
border="1px"
|
||||
borderColor="gray.200"
|
||||
borderColor="gray.700"
|
||||
borderRadius="md"
|
||||
bg="gray.900"
|
||||
>
|
||||
<HStack justify="space-between">
|
||||
<VStack align="start" spacing={1} flex={1}>
|
||||
<HStack spacing={2}>
|
||||
<Text fontWeight="bold" fontSize="sm">
|
||||
{match.song.title}
|
||||
<Text fontWeight="bold" fontSize="sm" color="white">
|
||||
{suggestion.song.title}
|
||||
</Text>
|
||||
<Badge colorScheme={getConfidenceColor(match.confidence)} size="sm">
|
||||
{(match.confidence * 100).toFixed(0)}%
|
||||
<Badge
|
||||
colorScheme={getConfidenceColor(suggestion.confidence)}
|
||||
size="sm"
|
||||
bg={`${getConfidenceColor(suggestion.confidence)}.900`}
|
||||
color={`${getConfidenceColor(suggestion.confidence)}.200`}
|
||||
>
|
||||
{Math.round(suggestion.confidence * 100)}%
|
||||
</Badge>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color="gray.600">
|
||||
{match.song.artist}
|
||||
<Text fontSize="xs" color="gray.400">
|
||||
{suggestion.song.artist}
|
||||
</Text>
|
||||
{match.song.album && (
|
||||
{suggestion.song.location && (
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{match.song.album}
|
||||
📁 {suggestion.song.location}
|
||||
</Text>
|
||||
)}
|
||||
{match.song.location && (
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
📁 {match.song.location}
|
||||
</Text>
|
||||
)}
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{match.matchReason}
|
||||
<Text fontSize="xs" color="blue.400">
|
||||
{suggestion.matchReason}
|
||||
</Text>
|
||||
</VStack>
|
||||
<Button
|
||||
leftIcon={<FiLink />}
|
||||
<Tooltip label="Link this song">
|
||||
<IconButton
|
||||
aria-label="Link"
|
||||
icon={<FiLink />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="blue"
|
||||
onClick={() => handleLinkMusicFile(match.musicFile._id, match.song._id)}
|
||||
>
|
||||
Link
|
||||
</Button>
|
||||
onClick={() => {
|
||||
handleLinkMusicFile(selectedMusicFile._id, suggestion.song._id);
|
||||
onClose();
|
||||
}}
|
||||
_hover={{ bg: "blue.900" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
</Box>
|
||||
))}
|
||||
@ -628,13 +635,12 @@ export const SongMatching: React.FC = () => {
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
<Button variant="ghost" onClick={onClose} color="gray.400" _hover={{ bg: "gray.700" }}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@ -171,40 +171,55 @@ export const MusicStorage: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box p={6} maxW="1200px" mx="auto">
|
||||
<Box
|
||||
p={6}
|
||||
maxW="1200px"
|
||||
mx="auto"
|
||||
minH="100vh"
|
||||
bg="gray.900"
|
||||
color="gray.100"
|
||||
>
|
||||
<VStack spacing={6} align="stretch">
|
||||
<Heading size="lg" textAlign="center">
|
||||
<Heading size="lg" textAlign="center" color="white">
|
||||
🎵 Music Storage & Playback
|
||||
</Heading>
|
||||
|
||||
<Alert status="info">
|
||||
<AlertIcon />
|
||||
<Alert status="info" bg="blue.900" borderColor="blue.700" color="blue.100">
|
||||
<AlertIcon color="blue.300" />
|
||||
<Box>
|
||||
<Text fontWeight="bold">S3 Storage Feature</Text>
|
||||
<Text fontSize="sm">
|
||||
<Text fontWeight="bold" color="blue.100">S3 Storage Feature</Text>
|
||||
<Text fontSize="sm" color="blue.200">
|
||||
Upload your music files to S3-compatible storage (MinIO) and stream them directly in the browser.
|
||||
Supports MP3, WAV, FLAC, AAC, OGG, WMA, and Opus formats.
|
||||
</Text>
|
||||
</Box>
|
||||
</Alert>
|
||||
|
||||
<Tabs variant="enclosed">
|
||||
<TabList>
|
||||
<Tab>Upload Music</Tab>
|
||||
<Tab>Music Library</Tab>
|
||||
<Tab>Song Matching</Tab>
|
||||
<Tab>Player</Tab>
|
||||
<Tabs variant="enclosed" colorScheme="blue">
|
||||
<TabList bg="gray.800" borderColor="gray.700">
|
||||
<Tab color="gray.300" _selected={{ bg: "gray.700", color: "white", borderColor: "gray.600" }}>
|
||||
Upload Music
|
||||
</Tab>
|
||||
<Tab color="gray.300" _selected={{ bg: "gray.700", color: "white", borderColor: "gray.600" }}>
|
||||
Music Library
|
||||
</Tab>
|
||||
<Tab color="gray.300" _selected={{ bg: "gray.700", color: "white", borderColor: "gray.600" }}>
|
||||
Song Matching
|
||||
</Tab>
|
||||
<Tab color="gray.300" _selected={{ bg: "gray.700", color: "white", borderColor: "gray.600" }}>
|
||||
Player
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
{/* Upload Tab */}
|
||||
<TabPanel>
|
||||
<TabPanel bg="gray.900">
|
||||
<VStack spacing={6} align="stretch">
|
||||
<Box>
|
||||
<Heading size="md" mb={4}>
|
||||
<Heading size="md" mb={4} color="white">
|
||||
Upload Music Files
|
||||
</Heading>
|
||||
<Text color="gray.600" mb={4}>
|
||||
<Text color="gray.400" mb={4}>
|
||||
Drag and drop your music files here or click to select. Files will be uploaded to S3 storage
|
||||
and metadata will be automatically extracted.
|
||||
</Text>
|
||||
@ -214,21 +229,23 @@ export const MusicStorage: React.FC = () => {
|
||||
</TabPanel>
|
||||
|
||||
{/* Library Tab */}
|
||||
<TabPanel>
|
||||
<TabPanel bg="gray.900">
|
||||
<VStack spacing={4} align="stretch">
|
||||
<HStack justify="space-between">
|
||||
<Heading size="md">Music Library</Heading>
|
||||
<Heading size="md" color="white">Music Library</Heading>
|
||||
<HStack spacing={2}>
|
||||
<Text color="gray.600">
|
||||
<Text color="gray.400">
|
||||
{musicFiles.length} file{musicFiles.length !== 1 ? 's' : ''}
|
||||
</Text>
|
||||
<Button
|
||||
leftIcon={isSyncing ? <Spinner size="sm" /> : <FiRefreshCw />}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="blue"
|
||||
onClick={handleSyncS3}
|
||||
isLoading={isSyncing}
|
||||
loadingText="Syncing..."
|
||||
_hover={{ bg: "blue.900", borderColor: "blue.400" }}
|
||||
>
|
||||
Sync S3
|
||||
</Button>
|
||||
@ -250,6 +267,8 @@ export const MusicStorage: React.FC = () => {
|
||||
onClick={handleSyncS3}
|
||||
isLoading={isSyncing}
|
||||
loadingText="Syncing..."
|
||||
colorScheme="blue"
|
||||
_hover={{ bg: "blue.700" }}
|
||||
>
|
||||
Sync S3 Bucket
|
||||
</Button>
|
||||
@ -257,10 +276,10 @@ export const MusicStorage: React.FC = () => {
|
||||
) : (
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
|
||||
{musicFiles.map((file) => (
|
||||
<Card key={file._id} size="sm">
|
||||
<Card key={file._id} size="sm" bg="gray.800" borderColor="gray.700" borderWidth="1px">
|
||||
<CardHeader pb={2}>
|
||||
<HStack justify="space-between">
|
||||
<Badge colorScheme="blue" variant="subtle">
|
||||
<Badge colorScheme="blue" variant="subtle" bg="blue.900" color="blue.200">
|
||||
{file.format?.toUpperCase() || 'AUDIO'}
|
||||
</Badge>
|
||||
<IconButton
|
||||
@ -270,16 +289,17 @@ export const MusicStorage: React.FC = () => {
|
||||
variant="ghost"
|
||||
colorScheme="red"
|
||||
onClick={() => handleDeleteFile(file._id)}
|
||||
_hover={{ bg: "red.900" }}
|
||||
/>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody pt={0}>
|
||||
<VStack spacing={2} align="stretch">
|
||||
<Text fontWeight="bold" fontSize="sm" noOfLines={1}>
|
||||
<Text fontWeight="bold" fontSize="sm" noOfLines={1} color="white">
|
||||
{file.title || file.originalName}
|
||||
</Text>
|
||||
{file.artist && (
|
||||
<Text fontSize="xs" color="gray.600" noOfLines={1}>
|
||||
<Text fontSize="xs" color="gray.400" noOfLines={1}>
|
||||
{file.artist}
|
||||
</Text>
|
||||
)}
|
||||
@ -293,7 +313,7 @@ export const MusicStorage: React.FC = () => {
|
||||
<Text>{formatFileSize(file.size)}</Text>
|
||||
</HStack>
|
||||
{file.songId && (
|
||||
<Badge colorScheme="green" size="sm" alignSelf="start">
|
||||
<Badge colorScheme="green" size="sm" alignSelf="start" bg="green.900" color="green.200">
|
||||
Linked to Rekordbox
|
||||
</Badge>
|
||||
)}
|
||||
@ -303,6 +323,7 @@ export const MusicStorage: React.FC = () => {
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
onClick={() => setSelectedFile(file)}
|
||||
_hover={{ bg: "blue.700" }}
|
||||
/>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
@ -314,14 +335,14 @@ export const MusicStorage: React.FC = () => {
|
||||
</TabPanel>
|
||||
|
||||
{/* Song Matching Tab */}
|
||||
<TabPanel>
|
||||
<TabPanel bg="gray.900">
|
||||
<SongMatching />
|
||||
</TabPanel>
|
||||
|
||||
{/* Player Tab */}
|
||||
<TabPanel>
|
||||
<TabPanel bg="gray.900">
|
||||
<VStack spacing={4} align="stretch">
|
||||
<Heading size="md">Music Player</Heading>
|
||||
<Heading size="md" color="white">Music Player</Heading>
|
||||
{selectedFile ? (
|
||||
<MusicPlayer
|
||||
musicFile={selectedFile}
|
||||
@ -339,9 +360,10 @@ export const MusicStorage: React.FC = () => {
|
||||
p={8}
|
||||
textAlign="center"
|
||||
border="2px dashed"
|
||||
borderColor="gray.300"
|
||||
borderColor="gray.600"
|
||||
borderRadius="lg"
|
||||
color="gray.500"
|
||||
bg="gray.800"
|
||||
>
|
||||
<FiMusic size={48} style={{ margin: '0 auto 16px' }} />
|
||||
<Text>Select a music file from the library to start playing</Text>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user