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:
Geert Rademakes 2025-08-06 15:01:40 +02:00
parent 7f186c6337
commit 3317a69004
3 changed files with 430 additions and 401 deletions

View File

@ -123,7 +123,7 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
<Box <Box
{...getRootProps()} {...getRootProps()}
border="2px dashed" border="2px dashed"
borderColor={isDragActive ? 'blue.400' : 'gray.300'} borderColor={isDragActive ? 'blue.400' : 'gray.600'}
borderRadius="lg" borderRadius="lg"
p={8} p={8}
textAlign="center" textAlign="center"
@ -131,19 +131,19 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
transition="all 0.2s" transition="all 0.2s"
_hover={{ _hover={{
borderColor: 'blue.400', borderColor: 'blue.400',
bg: 'blue.50', bg: 'blue.900',
}} }}
bg={isDragActive ? 'blue.50' : 'transparent'} bg={isDragActive ? 'blue.900' : 'gray.800'}
> >
<input {...getInputProps()} /> <input {...getInputProps()} />
<VStack spacing={3}> <VStack spacing={3}>
<Icon as={isDragActive ? FiUpload : FiMusic} w={8} h={8} color="blue.500" /> <Icon as={isDragActive ? FiUpload : FiMusic} w={8} h={8} color="blue.400" />
<Text fontSize="lg" fontWeight="medium"> <Text fontSize="lg" fontWeight="medium" color="white">
{isDragActive {isDragActive
? 'Drop the music files here...' ? 'Drop the music files here...'
: 'Drag & drop music files here, or click to select'} : 'Drag & drop music files here, or click to select'}
</Text> </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) Supports MP3, WAV, FLAC, AAC, OGG, WMA, Opus (max 100MB per file)
</Text> </Text>
</VStack> </VStack>
@ -152,30 +152,30 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
{uploadProgress.length > 0 && ( {uploadProgress.length > 0 && (
<VStack spacing={3} align="stretch"> <VStack spacing={3} align="stretch">
<HStack justify="space-between"> <HStack justify="space-between">
<Text fontWeight="medium">Upload Progress</Text> <Text fontWeight="medium" color="white">Upload Progress</Text>
<Button size="sm" variant="ghost" onClick={resetUploads}> <Button size="sm" variant="ghost" onClick={resetUploads} color="gray.400" _hover={{ bg: "gray.700" }}>
Clear Clear
</Button> </Button>
</HStack> </HStack>
{uploadProgress.map((item, index) => ( {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}> <HStack justify="space-between" mb={2}>
<Text fontSize="sm" fontWeight="medium" noOfLines={1}> <Text fontSize="sm" fontWeight="medium" noOfLines={1} color="white">
{item.fileName} {item.fileName}
</Text> </Text>
<Icon <Icon
as={item.status === 'success' ? FiCheck : item.status === 'error' ? FiX : undefined} 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> </HStack>
{item.status === 'error' ? ( {item.status === 'error' ? (
<Alert status="error" size="sm"> <Alert status="error" size="sm" bg="red.900" borderColor="red.700" color="red.100">
<AlertIcon /> <AlertIcon color="red.300" />
<Box> <Box>
<AlertTitle>Upload failed</AlertTitle> <AlertTitle color="red.100">Upload failed</AlertTitle>
<AlertDescription>{item.error}</AlertDescription> <AlertDescription color="red.200">{item.error}</AlertDescription>
</Box> </Box>
</Alert> </Alert>
) : ( ) : (
@ -183,6 +183,7 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
value={item.progress} value={item.progress}
colorScheme={item.status === 'success' ? 'green' : 'blue'} colorScheme={item.status === 'success' ? 'green' : 'blue'}
size="sm" size="sm"
bg="gray.700"
/> />
)} )}
</Box> </Box>
@ -191,11 +192,11 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
)} )}
{isUploading && ( {isUploading && (
<Alert status="info"> <Alert status="info" bg="blue.900" borderColor="blue.700" color="blue.100">
<AlertIcon /> <AlertIcon color="blue.300" />
<Box> <Box>
<AlertTitle>Uploading files...</AlertTitle> <AlertTitle color="blue.100">Uploading files...</AlertTitle>
<AlertDescription> <AlertDescription color="blue.200">
Please wait while your music files are being uploaded and processed. Please wait while your music files are being uploaded and processed.
</AlertDescription> </AlertDescription>
</Box> </Box>

View File

@ -31,6 +31,7 @@ import {
StatHelpText, StatHelpText,
IconButton, IconButton,
Tooltip, Tooltip,
Spinner,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { FiPlay, FiLink, FiSearch, FiZap, FiMusic, FiCheck, FiX } from 'react-icons/fi'; import { FiPlay, FiLink, FiSearch, FiZap, FiMusic, FiCheck, FiX } from 'react-icons/fi';
@ -273,368 +274,373 @@ export const SongMatching: React.FC = () => {
} }
return ( return (
<Box p={6} maxW="1200px" mx="auto"> <VStack spacing={6} align="stretch">
<VStack spacing={6} align="stretch"> {/* Statistics */}
<Heading size="lg" textAlign="center"> {stats && (
🎵 Song Matching & Linking <Card bg="gray.800" borderColor="gray.700" borderWidth="1px">
</Heading>
{/* Statistics */}
{stats && (
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
<Stat>
<StatLabel>Total Songs</StatLabel>
<StatNumber>{stats.totalSongs}</StatNumber>
<StatHelpText>In Rekordbox library</StatHelpText>
</Stat>
<Stat>
<StatLabel>Music Files</StatLabel>
<StatNumber>{stats.totalMusicFiles}</StatNumber>
<StatHelpText>Uploaded to S3</StatHelpText>
</Stat>
<Stat>
<StatLabel>Match Rate</StatLabel>
<StatNumber>{stats.matchRate}%</StatNumber>
<StatHelpText>{stats.matchedMusicFiles} of {stats.totalMusicFiles} linked</StatHelpText>
</Stat>
</SimpleGrid>
)}
{/* Auto-linking */}
<Card>
<CardHeader> <CardHeader>
<HStack justify="space-between"> <Heading size="md" color="white">Matching Statistics</Heading>
<Heading size="md">Auto-Linking</Heading>
<Button
leftIcon={<FiZap />}
colorScheme="blue"
onClick={handleAutoLink}
isLoading={autoLinking}
loadingText="Auto-linking..."
>
Auto-Link Files
</Button>
</HStack>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
<Text color="gray.600"> <SimpleGrid columns={{ base: 2, md: 4 }} spacing={4}>
Automatically match and link music files to songs in your Rekordbox library. <Stat>
This will attempt to find matches based on filename, title, artist, and other metadata. <StatLabel color="gray.400">Total Songs</StatLabel>
Original file paths are preserved and S3 information is added alongside. <StatNumber color="white">{stats.totalSongs}</StatNumber>
</Stat>
<Stat>
<StatLabel color="gray.400">Music Files</StatLabel>
<StatNumber color="white">{stats.totalMusicFiles}</StatNumber>
</Stat>
<Stat>
<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-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> </Text>
</CardBody> <Button
</Card> leftIcon={<FiZap />}
colorScheme="blue"
size="lg"
onClick={handleAutoLink}
isLoading={autoLinking}
loadingText="Auto-Linking..."
_hover={{ bg: "blue.700" }}
>
Auto-Link Files
</Button>
</VStack>
</CardBody>
</Card>
{/* Unmatched Music Files */} {/* Unmatched Music Files */}
<Card> <Card bg="gray.800" borderColor="gray.700" borderWidth="1px">
<CardHeader> <CardHeader>
<Heading size="md">Unmatched Music Files ({unmatchedMusicFiles.length})</Heading> <Heading size="md" color="white">Unmatched Music Files ({unmatchedMusicFiles.length})</Heading>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
{unmatchedMusicFiles.length === 0 ? ( {unmatchedMusicFiles.length === 0 ? (
<Text color="gray.500" textAlign="center"> <Text color="gray.500" textAlign="center">
All music files have been matched! 🎉 All music files are matched! 🎉
</Text> </Text>
) : ( ) : (
<VStack spacing={3} align="stretch"> <VStack spacing={3} align="stretch">
{unmatchedMusicFiles.slice(0, 10).map((musicFile) => ( {unmatchedMusicFiles.slice(0, 10).map((file) => (
<Box <Box
key={musicFile._id} key={file._id}
p={3} p={3}
border="1px" border="1px"
borderColor="gray.200" borderColor="gray.700"
borderRadius="md" borderRadius="md"
> bg="gray.900"
<HStack justify="space-between"> >
<VStack align="start" spacing={1} flex={1}> <HStack justify="space-between">
<Text fontWeight="bold" fontSize="sm"> <VStack align="start" spacing={1} flex={1}>
{musicFile.title || musicFile.originalName} <Text fontWeight="bold" fontSize="sm" color="white">
</Text> {file.title || file.originalName}
{musicFile.artist && ( </Text>
<Text fontSize="xs" color="gray.600"> <Text fontSize="xs" color="gray.400">
{musicFile.artist} {file.artist}
</Text> </Text>
)} <Text fontSize="xs" color="gray.500">
{musicFile.album && ( {file.album}
<Text fontSize="xs" color="gray.500"> </Text>
{musicFile.album} </VStack>
</Text> <HStack spacing={2}>
)} <Tooltip label="Get matching suggestions">
<HStack spacing={2} fontSize="xs" color="gray.500"> <IconButton
<Text>{formatDuration(musicFile.duration || 0)}</Text> aria-label="Get suggestions"
<Text></Text> icon={<FiSearch />}
<Text>{musicFile.format?.toUpperCase()}</Text>
</HStack>
</VStack>
<HStack spacing={2}>
<Tooltip label="Get matching suggestions">
<IconButton
aria-label="Get suggestions"
icon={<FiSearch />}
size="sm"
variant="ghost"
onClick={() => handleGetSuggestions(musicFile)}
/>
</Tooltip>
<Tooltip label="Play music file">
<IconButton
aria-label="Play"
icon={<FiPlay />}
size="sm"
variant="ghost"
colorScheme="blue"
/>
</Tooltip>
</HStack>
</HStack>
</Box>
))}
{unmatchedMusicFiles.length > 10 && (
<Text fontSize="sm" color="gray.500" textAlign="center">
Showing first 10 of {unmatchedMusicFiles.length} unmatched files
</Text>
)}
</VStack>
)}
</CardBody>
</Card>
{/* Matched Music Files */}
<Card>
<CardHeader>
<Heading size="md">Matched Music Files ({matchedMusicFiles.length})</Heading>
</CardHeader>
<CardBody>
{matchedMusicFiles.length === 0 ? (
<Text color="gray.500" textAlign="center">
No music files have been matched yet.
</Text>
) : (
<VStack spacing={3} align="stretch">
{matchedMusicFiles.slice(0, 10).map((musicFile) => (
<Box
key={musicFile._id}
p={3}
border="1px"
borderColor="green.200"
borderRadius="md"
bg="green.50"
>
<HStack justify="space-between">
<VStack align="start" spacing={1} flex={1}>
<HStack spacing={2}>
<Text fontWeight="bold" fontSize="sm">
{musicFile.title || musicFile.originalName}
</Text>
<Badge colorScheme="green" size="sm">
<FiCheck style={{ marginRight: '4px' }} />
Linked
</Badge>
</HStack>
{musicFile.artist && (
<Text fontSize="xs" color="gray.600">
{musicFile.artist}
</Text>
)}
{musicFile.songId && (
<Text fontSize="xs" color="blue.600">
{musicFile.songId.title} by {musicFile.songId.artist}
</Text>
)}
{musicFile.songId?.location && (
<Text fontSize="xs" color="gray.500">
📁 {musicFile.songId.location}
</Text>
)}
</VStack>
<HStack spacing={2}>
<Tooltip label="Unlink from song">
<IconButton
aria-label="Unlink"
icon={<FiX />}
size="sm"
variant="ghost"
colorScheme="red"
onClick={() => handleUnlinkMusicFile(musicFile.songId._id)}
/>
</Tooltip>
<Tooltip label="Play music file">
<IconButton
aria-label="Play"
icon={<FiPlay />}
size="sm"
variant="ghost"
colorScheme="blue"
/>
</Tooltip>
</HStack>
</HStack>
</Box>
))}
{matchedMusicFiles.length > 10 && (
<Text fontSize="sm" color="gray.500" textAlign="center">
Showing first 10 of {matchedMusicFiles.length} matched files
</Text>
)}
</VStack>
)}
</CardBody>
</Card>
{/* Songs with Music Files */}
<Card>
<CardHeader>
<Heading size="md">Songs with Music Files ({songsWithMusicFiles.length})</Heading>
</CardHeader>
<CardBody>
{songsWithMusicFiles.length === 0 ? (
<Text color="gray.500" textAlign="center">
No songs have music files linked yet.
</Text>
) : (
<VStack spacing={3} align="stretch">
{songsWithMusicFiles.slice(0, 10).map((song) => (
<Box
key={song._id}
p={3}
border="1px"
borderColor="blue.200"
borderRadius="md"
bg="blue.50"
>
<HStack justify="space-between">
<VStack align="start" spacing={1} flex={1}>
<HStack spacing={2}>
<Text fontWeight="bold" fontSize="sm">
{song.title}
</Text>
<Badge colorScheme="blue" size="sm">
<FiMusic style={{ marginRight: '4px' }} />
Has S3 File
</Badge>
</HStack>
<Text fontSize="xs" color="gray.600">
{song.artist}
</Text>
{song.location && (
<Text fontSize="xs" color="gray.500">
📁 {song.location}
</Text>
)}
{song.s3File?.streamingUrl && (
<Text fontSize="xs" color="green.600">
🎵 S3: {song.s3File.s3Key}
</Text>
)}
</VStack>
<HStack spacing={2}>
<Tooltip label="Unlink music file">
<IconButton
aria-label="Unlink"
icon={<FiX />}
size="sm"
variant="ghost"
colorScheme="red"
onClick={() => handleUnlinkMusicFile(song._id)}
/>
</Tooltip>
<Tooltip label="Play music file">
<IconButton
aria-label="Play"
icon={<FiPlay />}
size="sm"
variant="ghost"
colorScheme="blue"
/>
</Tooltip>
</HStack>
</HStack>
</Box>
))}
{songsWithMusicFiles.length > 10 && (
<Text fontSize="sm" color="gray.500" textAlign="center">
Showing first 10 of {songsWithMusicFiles.length} songs with music files
</Text>
)}
</VStack>
)}
</CardBody>
</Card>
{/* Suggestions Modal */}
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>
Matching Suggestions for "{selectedMusicFile?.title || selectedMusicFile?.originalName}"
</ModalHeader>
<ModalCloseButton />
<ModalBody>
{loadingSuggestions ? (
<Progress size="xs" isIndeterminate />
) : suggestions.length === 0 ? (
<Text color="gray.500" textAlign="center">
No matching suggestions found.
</Text>
) : (
<VStack spacing={3} align="stretch">
{suggestions.map((match, index) => (
<Box
key={index}
p={3}
border="1px"
borderColor="gray.200"
borderRadius="md"
>
<HStack justify="space-between">
<VStack align="start" spacing={1} flex={1}>
<HStack spacing={2}>
<Text fontWeight="bold" fontSize="sm">
{match.song.title}
</Text>
<Badge colorScheme={getConfidenceColor(match.confidence)} size="sm">
{(match.confidence * 100).toFixed(0)}%
</Badge>
</HStack>
<Text fontSize="xs" color="gray.600">
{match.song.artist}
</Text>
{match.song.album && (
<Text fontSize="xs" color="gray.500">
{match.song.album}
</Text>
)}
{match.song.location && (
<Text fontSize="xs" color="gray.500">
📁 {match.song.location}
</Text>
)}
<Text fontSize="xs" color="gray.500">
{match.matchReason}
</Text>
</VStack>
<Button
leftIcon={<FiLink />}
size="sm" size="sm"
variant="ghost"
colorScheme="blue" colorScheme="blue"
onClick={() => handleLinkMusicFile(match.musicFile._id, match.song._id)} onClick={() => handleGetSuggestions(file)}
> _hover={{ bg: "blue.900" }}
Link />
</Button> </Tooltip>
</HStack> <Tooltip label="Play music file">
</Box> <IconButton
))} aria-label="Play"
</VStack> icon={<FiPlay />}
size="sm"
variant="ghost"
colorScheme="green"
_hover={{ bg: "green.900" }}
/>
</Tooltip>
</HStack>
</HStack>
</Box>
))}
{unmatchedMusicFiles.length > 10 && (
<Text fontSize="sm" color="gray.500" textAlign="center">
Showing first 10 of {unmatchedMusicFiles.length} unmatched files
</Text>
)} )}
</ModalBody> </VStack>
<ModalFooter> )}
<Button variant="ghost" onClick={onClose}> </CardBody>
Close </Card>
</Button>
</ModalFooter> {/* Matched Music Files */}
</ModalContent> <Card bg="gray.800" borderColor="gray.700" borderWidth="1px">
</Modal> <CardHeader>
</VStack> <Heading size="md" color="white">Matched Music Files ({matchedMusicFiles.length})</Heading>
</Box> </CardHeader>
<CardBody>
{matchedMusicFiles.length === 0 ? (
<Text color="gray.500" textAlign="center">
No music files are matched yet.
</Text>
) : (
<VStack spacing={3} align="stretch">
{matchedMusicFiles.slice(0, 10).map((file) => (
<Box
key={file._id}
p={3}
border="1px"
borderColor="green.700"
borderRadius="md"
bg="green.900"
>
<HStack justify="space-between">
<VStack align="start" spacing={1} flex={1}>
<HStack spacing={2}>
<Text fontWeight="bold" fontSize="sm" color="white">
{file.title || file.originalName}
</Text>
<Badge colorScheme="green" size="sm" bg="green.800" color="green.200">
<FiCheck style={{ marginRight: '4px' }} />
Matched
</Badge>
</HStack>
<Text fontSize="xs" color="gray.300">
{file.artist}
</Text>
<Text fontSize="xs" color="gray.400">
{file.album}
</Text>
</VStack>
<HStack spacing={2}>
<Tooltip label="Unlink music file">
<IconButton
aria-label="Unlink"
icon={<FiX />}
size="sm"
variant="ghost"
colorScheme="red"
onClick={() => handleUnlinkMusicFile(file.songId)}
_hover={{ bg: "red.900" }}
/>
</Tooltip>
<Tooltip label="Play music file">
<IconButton
aria-label="Play"
icon={<FiPlay />}
size="sm"
variant="ghost"
colorScheme="blue"
_hover={{ bg: "blue.900" }}
/>
</Tooltip>
</HStack>
</HStack>
</Box>
))}
{matchedMusicFiles.length > 10 && (
<Text fontSize="sm" color="gray.500" textAlign="center">
Showing first 10 of {matchedMusicFiles.length} matched files
</Text>
)}
</VStack>
)}
</CardBody>
</Card>
{/* Songs with Music Files */}
<Card bg="gray.800" borderColor="gray.700" borderWidth="1px">
<CardHeader>
<Heading size="md" color="white">Songs with Music Files ({songsWithMusicFiles.length})</Heading>
</CardHeader>
<CardBody>
{songsWithMusicFiles.length === 0 ? (
<Text color="gray.500" textAlign="center">
No songs have music files linked yet.
</Text>
) : (
<VStack spacing={3} align="stretch">
{songsWithMusicFiles.slice(0, 10).map((song) => (
<Box
key={song._id}
p={3}
border="1px"
borderColor="blue.700"
borderRadius="md"
bg="blue.900"
>
<HStack justify="space-between">
<VStack align="start" spacing={1} flex={1}>
<HStack spacing={2}>
<Text fontWeight="bold" fontSize="sm" color="white">
{song.title}
</Text>
<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.300">
{song.artist}
</Text>
{song.location && (
<Text fontSize="xs" color="gray.400">
📁 {song.location}
</Text>
)}
{song.s3File?.streamingUrl && (
<Text fontSize="xs" color="green.400">
🎵 S3: {song.s3File.s3Key}
</Text>
)}
</VStack>
<HStack spacing={2}>
<Tooltip label="Unlink music file">
<IconButton
aria-label="Unlink"
icon={<FiX />}
size="sm"
variant="ghost"
colorScheme="red"
onClick={() => handleUnlinkMusicFile(song._id)}
_hover={{ bg: "red.900" }}
/>
</Tooltip>
<Tooltip label="Play music file">
<IconButton
aria-label="Play"
icon={<FiPlay />}
size="sm"
variant="ghost"
colorScheme="blue"
_hover={{ bg: "blue.800" }}
/>
</Tooltip>
</HStack>
</HStack>
</Box>
))}
{songsWithMusicFiles.length > 10 && (
<Text fontSize="sm" color="gray.500" textAlign="center">
Showing first 10 of {songsWithMusicFiles.length} songs with music files
</Text>
)}
</VStack>
)}
</CardBody>
</Card>
{/* Suggestions Modal */}
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent bg="gray.800" borderColor="gray.700" borderWidth="1px">
<ModalHeader color="white">
Matching Suggestions for "{selectedMusicFile?.title || selectedMusicFile?.originalName}"
</ModalHeader>
<ModalCloseButton color="gray.400" />
<ModalBody>
{loadingSuggestions ? (
<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 songs found. You can manually link this file later.
</Text>
) : (
<VStack spacing={3} align="stretch">
{suggestions.map((suggestion, index) => (
<Box
key={index}
p={3}
border="1px"
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" color="white">
{suggestion.song.title}
</Text>
<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.400">
{suggestion.song.artist}
</Text>
{suggestion.song.location && (
<Text fontSize="xs" color="gray.500">
📁 {suggestion.song.location}
</Text>
)}
<Text fontSize="xs" color="blue.400">
{suggestion.matchReason}
</Text>
</VStack>
<Tooltip label="Link this song">
<IconButton
aria-label="Link"
icon={<FiLink />}
size="sm"
variant="ghost"
colorScheme="blue"
onClick={() => {
handleLinkMusicFile(selectedMusicFile._id, suggestion.song._id);
onClose();
}}
_hover={{ bg: "blue.900" }}
/>
</Tooltip>
</HStack>
</Box>
))}
</VStack>
)}
</ModalBody>
<ModalFooter>
<Button variant="ghost" onClick={onClose} color="gray.400" _hover={{ bg: "gray.700" }}>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</VStack>
); );
}; };

View File

@ -171,40 +171,55 @@ export const MusicStorage: React.FC = () => {
}; };
return ( 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"> <VStack spacing={6} align="stretch">
<Heading size="lg" textAlign="center"> <Heading size="lg" textAlign="center" color="white">
🎵 Music Storage & Playback 🎵 Music Storage & Playback
</Heading> </Heading>
<Alert status="info"> <Alert status="info" bg="blue.900" borderColor="blue.700" color="blue.100">
<AlertIcon /> <AlertIcon color="blue.300" />
<Box> <Box>
<Text fontWeight="bold">S3 Storage Feature</Text> <Text fontWeight="bold" color="blue.100">S3 Storage Feature</Text>
<Text fontSize="sm"> <Text fontSize="sm" color="blue.200">
Upload your music files to S3-compatible storage (MinIO) and stream them directly in the browser. 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. Supports MP3, WAV, FLAC, AAC, OGG, WMA, and Opus formats.
</Text> </Text>
</Box> </Box>
</Alert> </Alert>
<Tabs variant="enclosed"> <Tabs variant="enclosed" colorScheme="blue">
<TabList> <TabList bg="gray.800" borderColor="gray.700">
<Tab>Upload Music</Tab> <Tab color="gray.300" _selected={{ bg: "gray.700", color: "white", borderColor: "gray.600" }}>
<Tab>Music Library</Tab> Upload Music
<Tab>Song Matching</Tab> </Tab>
<Tab>Player</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> </TabList>
<TabPanels> <TabPanels>
{/* Upload Tab */} {/* Upload Tab */}
<TabPanel> <TabPanel bg="gray.900">
<VStack spacing={6} align="stretch"> <VStack spacing={6} align="stretch">
<Box> <Box>
<Heading size="md" mb={4}> <Heading size="md" mb={4} color="white">
Upload Music Files Upload Music Files
</Heading> </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 Drag and drop your music files here or click to select. Files will be uploaded to S3 storage
and metadata will be automatically extracted. and metadata will be automatically extracted.
</Text> </Text>
@ -214,21 +229,23 @@ export const MusicStorage: React.FC = () => {
</TabPanel> </TabPanel>
{/* Library Tab */} {/* Library Tab */}
<TabPanel> <TabPanel bg="gray.900">
<VStack spacing={4} align="stretch"> <VStack spacing={4} align="stretch">
<HStack justify="space-between"> <HStack justify="space-between">
<Heading size="md">Music Library</Heading> <Heading size="md" color="white">Music Library</Heading>
<HStack spacing={2}> <HStack spacing={2}>
<Text color="gray.600"> <Text color="gray.400">
{musicFiles.length} file{musicFiles.length !== 1 ? 's' : ''} {musicFiles.length} file{musicFiles.length !== 1 ? 's' : ''}
</Text> </Text>
<Button <Button
leftIcon={isSyncing ? <Spinner size="sm" /> : <FiRefreshCw />} leftIcon={isSyncing ? <Spinner size="sm" /> : <FiRefreshCw />}
size="sm" size="sm"
variant="outline" variant="outline"
colorScheme="blue"
onClick={handleSyncS3} onClick={handleSyncS3}
isLoading={isSyncing} isLoading={isSyncing}
loadingText="Syncing..." loadingText="Syncing..."
_hover={{ bg: "blue.900", borderColor: "blue.400" }}
> >
Sync S3 Sync S3
</Button> </Button>
@ -250,6 +267,8 @@ export const MusicStorage: React.FC = () => {
onClick={handleSyncS3} onClick={handleSyncS3}
isLoading={isSyncing} isLoading={isSyncing}
loadingText="Syncing..." loadingText="Syncing..."
colorScheme="blue"
_hover={{ bg: "blue.700" }}
> >
Sync S3 Bucket Sync S3 Bucket
</Button> </Button>
@ -257,10 +276,10 @@ export const MusicStorage: React.FC = () => {
) : ( ) : (
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}> <SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
{musicFiles.map((file) => ( {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}> <CardHeader pb={2}>
<HStack justify="space-between"> <HStack justify="space-between">
<Badge colorScheme="blue" variant="subtle"> <Badge colorScheme="blue" variant="subtle" bg="blue.900" color="blue.200">
{file.format?.toUpperCase() || 'AUDIO'} {file.format?.toUpperCase() || 'AUDIO'}
</Badge> </Badge>
<IconButton <IconButton
@ -270,16 +289,17 @@ export const MusicStorage: React.FC = () => {
variant="ghost" variant="ghost"
colorScheme="red" colorScheme="red"
onClick={() => handleDeleteFile(file._id)} onClick={() => handleDeleteFile(file._id)}
_hover={{ bg: "red.900" }}
/> />
</HStack> </HStack>
</CardHeader> </CardHeader>
<CardBody pt={0}> <CardBody pt={0}>
<VStack spacing={2} align="stretch"> <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} {file.title || file.originalName}
</Text> </Text>
{file.artist && ( {file.artist && (
<Text fontSize="xs" color="gray.600" noOfLines={1}> <Text fontSize="xs" color="gray.400" noOfLines={1}>
{file.artist} {file.artist}
</Text> </Text>
)} )}
@ -293,7 +313,7 @@ export const MusicStorage: React.FC = () => {
<Text>{formatFileSize(file.size)}</Text> <Text>{formatFileSize(file.size)}</Text>
</HStack> </HStack>
{file.songId && ( {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 Linked to Rekordbox
</Badge> </Badge>
)} )}
@ -303,6 +323,7 @@ export const MusicStorage: React.FC = () => {
size="sm" size="sm"
colorScheme="blue" colorScheme="blue"
onClick={() => setSelectedFile(file)} onClick={() => setSelectedFile(file)}
_hover={{ bg: "blue.700" }}
/> />
</VStack> </VStack>
</CardBody> </CardBody>
@ -314,14 +335,14 @@ export const MusicStorage: React.FC = () => {
</TabPanel> </TabPanel>
{/* Song Matching Tab */} {/* Song Matching Tab */}
<TabPanel> <TabPanel bg="gray.900">
<SongMatching /> <SongMatching />
</TabPanel> </TabPanel>
{/* Player Tab */} {/* Player Tab */}
<TabPanel> <TabPanel bg="gray.900">
<VStack spacing={4} align="stretch"> <VStack spacing={4} align="stretch">
<Heading size="md">Music Player</Heading> <Heading size="md" color="white">Music Player</Heading>
{selectedFile ? ( {selectedFile ? (
<MusicPlayer <MusicPlayer
musicFile={selectedFile} musicFile={selectedFile}
@ -339,9 +360,10 @@ export const MusicStorage: React.FC = () => {
p={8} p={8}
textAlign="center" textAlign="center"
border="2px dashed" border="2px dashed"
borderColor="gray.300" borderColor="gray.600"
borderRadius="lg" borderRadius="lg"
color="gray.500" color="gray.500"
bg="gray.800"
> >
<FiMusic size={48} style={{ margin: '0 auto 16px' }} /> <FiMusic size={48} style={{ margin: '0 auto 16px' }} />
<Text>Select a music file from the library to start playing</Text> <Text>Select a music file from the library to start playing</Text>