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
|
<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>
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user