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,98 +274,96 @@ export const SongMatching: React.FC = () => {
} }
return ( return (
<Box p={6} maxW="1200px" mx="auto">
<VStack spacing={6} align="stretch"> <VStack spacing={6} align="stretch">
<Heading size="lg" textAlign="center">
🎵 Song Matching & Linking
</Heading>
{/* Statistics */} {/* Statistics */}
{stats && ( {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> <Stat>
<StatLabel>Total Songs</StatLabel> <StatLabel color="gray.400">Total Songs</StatLabel>
<StatNumber>{stats.totalSongs}</StatNumber> <StatNumber color="white">{stats.totalSongs}</StatNumber>
<StatHelpText>In Rekordbox library</StatHelpText>
</Stat> </Stat>
<Stat> <Stat>
<StatLabel>Music Files</StatLabel> <StatLabel color="gray.400">Music Files</StatLabel>
<StatNumber>{stats.totalMusicFiles}</StatNumber> <StatNumber color="white">{stats.totalMusicFiles}</StatNumber>
<StatHelpText>Uploaded to S3</StatHelpText>
</Stat> </Stat>
<Stat> <Stat>
<StatLabel>Match Rate</StatLabel> <StatLabel color="gray.400">Match Rate</StatLabel>
<StatNumber>{stats.matchRate}%</StatNumber> <StatNumber color="green.400">{stats.matchRate}</StatNumber>
<StatHelpText>{stats.matchedMusicFiles} of {stats.totalMusicFiles} linked</StatHelpText> <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> </Stat>
</SimpleGrid> </SimpleGrid>
</CardBody>
</Card>
)} )}
{/* Auto-linking */} {/* Auto-Link Button */}
<Card> <Card bg="gray.800" borderColor="gray.700" borderWidth="1px">
<CardHeader> <CardBody>
<HStack justify="space-between"> <VStack spacing={4}>
<Heading size="md">Auto-Linking</Heading> <Text color="gray.300" textAlign="center">
Automatically match and link music files to songs in your Rekordbox library
</Text>
<Button <Button
leftIcon={<FiZap />} leftIcon={<FiZap />}
colorScheme="blue" colorScheme="blue"
size="lg"
onClick={handleAutoLink} onClick={handleAutoLink}
isLoading={autoLinking} isLoading={autoLinking}
loadingText="Auto-linking..." loadingText="Auto-Linking..."
_hover={{ bg: "blue.700" }}
> >
Auto-Link Files Auto-Link Files
</Button> </Button>
</HStack> </VStack>
</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>
</CardBody> </CardBody>
</Card> </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"> <HStack justify="space-between">
<VStack align="start" spacing={1} flex={1}> <VStack align="start" spacing={1} flex={1}>
<Text fontWeight="bold" fontSize="sm"> <Text fontWeight="bold" fontSize="sm" color="white">
{musicFile.title || musicFile.originalName} {file.title || file.originalName}
</Text> </Text>
{musicFile.artist && ( <Text fontSize="xs" color="gray.400">
<Text fontSize="xs" color="gray.600"> {file.artist}
{musicFile.artist}
</Text> </Text>
)}
{musicFile.album && (
<Text fontSize="xs" color="gray.500"> <Text fontSize="xs" color="gray.500">
{musicFile.album} {file.album}
</Text> </Text>
)}
<HStack spacing={2} fontSize="xs" color="gray.500">
<Text>{formatDuration(musicFile.duration || 0)}</Text>
<Text></Text>
<Text>{musicFile.format?.toUpperCase()}</Text>
</HStack>
</VStack> </VStack>
<HStack spacing={2}> <HStack spacing={2}>
<Tooltip label="Get matching suggestions"> <Tooltip label="Get matching suggestions">
@ -373,7 +372,9 @@ export const SongMatching: React.FC = () => {
icon={<FiSearch />} icon={<FiSearch />}
size="sm" size="sm"
variant="ghost" variant="ghost"
onClick={() => handleGetSuggestions(musicFile)} colorScheme="blue"
onClick={() => handleGetSuggestions(file)}
_hover={{ bg: "blue.900" }}
/> />
</Tooltip> </Tooltip>
<Tooltip label="Play music file"> <Tooltip label="Play music file">
@ -382,7 +383,8 @@ export const SongMatching: React.FC = () => {
icon={<FiPlay />} icon={<FiPlay />}
size="sm" size="sm"
variant="ghost" variant="ghost"
colorScheme="blue" colorScheme="green"
_hover={{ bg: "green.900" }}
/> />
</Tooltip> </Tooltip>
</HStack> </HStack>
@ -400,62 +402,54 @@ export const SongMatching: React.FC = () => {
</Card> </Card>
{/* Matched Music Files */} {/* Matched Music Files */}
<Card> <Card bg="gray.800" borderColor="gray.700" borderWidth="1px">
<CardHeader> <CardHeader>
<Heading size="md">Matched Music Files ({matchedMusicFiles.length})</Heading> <Heading size="md" color="white">Matched Music Files ({matchedMusicFiles.length})</Heading>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
{matchedMusicFiles.length === 0 ? ( {matchedMusicFiles.length === 0 ? (
<Text color="gray.500" textAlign="center"> <Text color="gray.500" textAlign="center">
No music files have been matched yet. No music files are matched yet.
</Text> </Text>
) : ( ) : (
<VStack spacing={3} align="stretch"> <VStack spacing={3} align="stretch">
{matchedMusicFiles.slice(0, 10).map((musicFile) => ( {matchedMusicFiles.slice(0, 10).map((file) => (
<Box <Box
key={musicFile._id} key={file._id}
p={3} p={3}
border="1px" border="1px"
borderColor="green.200" borderColor="green.700"
borderRadius="md" borderRadius="md"
bg="green.50" bg="green.900"
> >
<HStack justify="space-between"> <HStack justify="space-between">
<VStack align="start" spacing={1} flex={1}> <VStack align="start" spacing={1} flex={1}>
<HStack spacing={2}> <HStack spacing={2}>
<Text fontWeight="bold" fontSize="sm"> <Text fontWeight="bold" fontSize="sm" color="white">
{musicFile.title || musicFile.originalName} {file.title || file.originalName}
</Text> </Text>
<Badge colorScheme="green" size="sm"> <Badge colorScheme="green" size="sm" bg="green.800" color="green.200">
<FiCheck style={{ marginRight: '4px' }} /> <FiCheck style={{ marginRight: '4px' }} />
Linked Matched
</Badge> </Badge>
</HStack> </HStack>
{musicFile.artist && ( <Text fontSize="xs" color="gray.300">
<Text fontSize="xs" color="gray.600"> {file.artist}
{musicFile.artist}
</Text> </Text>
)} <Text fontSize="xs" color="gray.400">
{musicFile.songId && ( {file.album}
<Text fontSize="xs" color="blue.600">
{musicFile.songId.title} by {musicFile.songId.artist}
</Text> </Text>
)}
{musicFile.songId?.location && (
<Text fontSize="xs" color="gray.500">
📁 {musicFile.songId.location}
</Text>
)}
</VStack> </VStack>
<HStack spacing={2}> <HStack spacing={2}>
<Tooltip label="Unlink from song"> <Tooltip label="Unlink music file">
<IconButton <IconButton
aria-label="Unlink" aria-label="Unlink"
icon={<FiX />} icon={<FiX />}
size="sm" size="sm"
variant="ghost" variant="ghost"
colorScheme="red" colorScheme="red"
onClick={() => handleUnlinkMusicFile(musicFile.songId._id)} onClick={() => handleUnlinkMusicFile(file.songId)}
_hover={{ bg: "red.900" }}
/> />
</Tooltip> </Tooltip>
<Tooltip label="Play music file"> <Tooltip label="Play music file">
@ -465,6 +459,7 @@ export const SongMatching: React.FC = () => {
size="sm" size="sm"
variant="ghost" variant="ghost"
colorScheme="blue" colorScheme="blue"
_hover={{ bg: "blue.900" }}
/> />
</Tooltip> </Tooltip>
</HStack> </HStack>
@ -482,9 +477,9 @@ export const SongMatching: React.FC = () => {
</Card> </Card>
{/* Songs with Music Files */} {/* Songs with Music Files */}
<Card> <Card bg="gray.800" borderColor="gray.700" borderWidth="1px">
<CardHeader> <CardHeader>
<Heading size="md">Songs with Music Files ({songsWithMusicFiles.length})</Heading> <Heading size="md" color="white">Songs with Music Files ({songsWithMusicFiles.length})</Heading>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
{songsWithMusicFiles.length === 0 ? ( {songsWithMusicFiles.length === 0 ? (
@ -498,31 +493,31 @@ export const SongMatching: React.FC = () => {
key={song._id} key={song._id}
p={3} p={3}
border="1px" border="1px"
borderColor="blue.200" borderColor="blue.700"
borderRadius="md" borderRadius="md"
bg="blue.50" bg="blue.900"
> >
<HStack justify="space-between"> <HStack justify="space-between">
<VStack align="start" spacing={1} flex={1}> <VStack align="start" spacing={1} flex={1}>
<HStack spacing={2}> <HStack spacing={2}>
<Text fontWeight="bold" fontSize="sm"> <Text fontWeight="bold" fontSize="sm" color="white">
{song.title} {song.title}
</Text> </Text>
<Badge colorScheme="blue" size="sm"> <Badge colorScheme="blue" size="sm" bg="blue.800" color="blue.200">
<FiMusic style={{ marginRight: '4px' }} /> <FiMusic style={{ marginRight: '4px' }} />
Has S3 File Has S3 File
</Badge> </Badge>
</HStack> </HStack>
<Text fontSize="xs" color="gray.600"> <Text fontSize="xs" color="gray.300">
{song.artist} {song.artist}
</Text> </Text>
{song.location && ( {song.location && (
<Text fontSize="xs" color="gray.500"> <Text fontSize="xs" color="gray.400">
📁 {song.location} 📁 {song.location}
</Text> </Text>
)} )}
{song.s3File?.streamingUrl && ( {song.s3File?.streamingUrl && (
<Text fontSize="xs" color="green.600"> <Text fontSize="xs" color="green.400">
🎵 S3: {song.s3File.s3Key} 🎵 S3: {song.s3File.s3Key}
</Text> </Text>
)} )}
@ -536,6 +531,7 @@ export const SongMatching: React.FC = () => {
variant="ghost" variant="ghost"
colorScheme="red" colorScheme="red"
onClick={() => handleUnlinkMusicFile(song._id)} onClick={() => handleUnlinkMusicFile(song._id)}
_hover={{ bg: "red.900" }}
/> />
</Tooltip> </Tooltip>
<Tooltip label="Play music file"> <Tooltip label="Play music file">
@ -545,6 +541,7 @@ export const SongMatching: React.FC = () => {
size="sm" size="sm"
variant="ghost" variant="ghost"
colorScheme="blue" colorScheme="blue"
_hover={{ bg: "blue.800" }}
/> />
</Tooltip> </Tooltip>
</HStack> </HStack>
@ -564,63 +561,73 @@ export const SongMatching: React.FC = () => {
{/* Suggestions Modal */} {/* Suggestions Modal */}
<Modal isOpen={isOpen} onClose={onClose} size="xl"> <Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent bg="gray.800" borderColor="gray.700" borderWidth="1px">
<ModalHeader> <ModalHeader color="white">
Matching Suggestions for "{selectedMusicFile?.title || selectedMusicFile?.originalName}" Matching Suggestions for "{selectedMusicFile?.title || selectedMusicFile?.originalName}"
</ModalHeader> </ModalHeader>
<ModalCloseButton /> <ModalCloseButton color="gray.400" />
<ModalBody> <ModalBody>
{loadingSuggestions ? ( {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 ? ( ) : suggestions.length === 0 ? (
<Text color="gray.500" textAlign="center"> <Text color="gray.500" textAlign="center">
No matching suggestions found. No matching songs found. You can manually link this file later.
</Text> </Text>
) : ( ) : (
<VStack spacing={3} align="stretch"> <VStack spacing={3} align="stretch">
{suggestions.map((match, index) => ( {suggestions.map((suggestion, index) => (
<Box <Box
key={index} key={index}
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"> <HStack justify="space-between">
<VStack align="start" spacing={1} flex={1}> <VStack align="start" spacing={1} flex={1}>
<HStack spacing={2}> <HStack spacing={2}>
<Text fontWeight="bold" fontSize="sm"> <Text fontWeight="bold" fontSize="sm" color="white">
{match.song.title} {suggestion.song.title}
</Text> </Text>
<Badge colorScheme={getConfidenceColor(match.confidence)} size="sm"> <Badge
{(match.confidence * 100).toFixed(0)}% colorScheme={getConfidenceColor(suggestion.confidence)}
size="sm"
bg={`${getConfidenceColor(suggestion.confidence)}.900`}
color={`${getConfidenceColor(suggestion.confidence)}.200`}
>
{Math.round(suggestion.confidence * 100)}%
</Badge> </Badge>
</HStack> </HStack>
<Text fontSize="xs" color="gray.600"> <Text fontSize="xs" color="gray.400">
{match.song.artist} {suggestion.song.artist}
</Text> </Text>
{match.song.album && ( {suggestion.song.location && (
<Text fontSize="xs" color="gray.500"> <Text fontSize="xs" color="gray.500">
{match.song.album} 📁 {suggestion.song.location}
</Text> </Text>
)} )}
{match.song.location && ( <Text fontSize="xs" color="blue.400">
<Text fontSize="xs" color="gray.500"> {suggestion.matchReason}
📁 {match.song.location}
</Text>
)}
<Text fontSize="xs" color="gray.500">
{match.matchReason}
</Text> </Text>
</VStack> </VStack>
<Button <Tooltip label="Link this song">
leftIcon={<FiLink />} <IconButton
aria-label="Link"
icon={<FiLink />}
size="sm" size="sm"
variant="ghost"
colorScheme="blue" colorScheme="blue"
onClick={() => handleLinkMusicFile(match.musicFile._id, match.song._id)} onClick={() => {
> handleLinkMusicFile(selectedMusicFile._id, suggestion.song._id);
Link onClose();
</Button> }}
_hover={{ bg: "blue.900" }}
/>
</Tooltip>
</HStack> </HStack>
</Box> </Box>
))} ))}
@ -628,13 +635,12 @@ export const SongMatching: React.FC = () => {
)} )}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button variant="ghost" onClick={onClose}> <Button variant="ghost" onClick={onClose} color="gray.400" _hover={{ bg: "gray.700" }}>
Close Close
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
</Modal> </Modal>
</VStack> </VStack>
</Box>
); );
}; };

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>