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
{...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>

View File

@ -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,368 +274,373 @@ 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}>
<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>
<VStack spacing={6} align="stretch">
{/* Statistics */}
{stats && (
<Card bg="gray.800" borderColor="gray.700" borderWidth="1px">
<CardHeader>
<HStack justify="space-between">
<Heading size="md">Auto-Linking</Heading>
<Button
leftIcon={<FiZap />}
colorScheme="blue"
onClick={handleAutoLink}
isLoading={autoLinking}
loadingText="Auto-linking..."
>
Auto-Link Files
</Button>
</HStack>
<Heading size="md" color="white">Matching Statistics</Heading>
</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.
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4}>
<Stat>
<StatLabel color="gray.400">Total Songs</StatLabel>
<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>
</CardBody>
</Card>
<Button
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 */}
<Card>
<CardHeader>
<Heading size="md">Unmatched Music Files ({unmatchedMusicFiles.length})</Heading>
</CardHeader>
<CardBody>
{unmatchedMusicFiles.length === 0 ? (
<Text color="gray.500" textAlign="center">
All music files have been matched! 🎉
</Text>
) : (
<VStack spacing={3} align="stretch">
{unmatchedMusicFiles.slice(0, 10).map((musicFile) => (
<Box
key={musicFile._id}
p={3}
border="1px"
borderColor="gray.200"
borderRadius="md"
>
<HStack justify="space-between">
<VStack align="start" spacing={1} flex={1}>
<Text fontWeight="bold" fontSize="sm">
{musicFile.title || musicFile.originalName}
</Text>
{musicFile.artist && (
<Text fontSize="xs" color="gray.600">
{musicFile.artist}
</Text>
)}
{musicFile.album && (
<Text fontSize="xs" color="gray.500">
{musicFile.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">
<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 />}
{/* Unmatched Music Files */}
<Card bg="gray.800" borderColor="gray.700" borderWidth="1px">
<CardHeader>
<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 are matched! 🎉
</Text>
) : (
<VStack spacing={3} align="stretch">
{unmatchedMusicFiles.slice(0, 10).map((file) => (
<Box
key={file._id}
p={3}
border="1px"
borderColor="gray.700"
borderRadius="md"
bg="gray.900"
>
<HStack justify="space-between">
<VStack align="start" spacing={1} flex={1}>
<Text fontWeight="bold" fontSize="sm" color="white">
{file.title || file.originalName}
</Text>
<Text fontSize="xs" color="gray.400">
{file.artist}
</Text>
<Text fontSize="xs" color="gray.500">
{file.album}
</Text>
</VStack>
<HStack spacing={2}>
<Tooltip label="Get matching suggestions">
<IconButton
aria-label="Get suggestions"
icon={<FiSearch />}
size="sm"
variant="ghost"
colorScheme="blue"
onClick={() => handleLinkMusicFile(match.musicFile._id, match.song._id)}
>
Link
</Button>
</HStack>
</Box>
))}
</VStack>
onClick={() => handleGetSuggestions(file)}
_hover={{ bg: "blue.900" }}
/>
</Tooltip>
<Tooltip label="Play music file">
<IconButton
aria-label="Play"
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>
<ModalFooter>
<Button variant="ghost" onClick={onClose}>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</VStack>
</Box>
</VStack>
)}
</CardBody>
</Card>
{/* Matched Music Files */}
<Card bg="gray.800" borderColor="gray.700" borderWidth="1px">
<CardHeader>
<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 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 (
<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>