Added config page!
This commit is contained in:
parent
5c1cf64c4c
commit
db4408b953
@ -1,10 +1,11 @@
|
||||
import { Box, Button, Flex, Heading, Input, Spinner, Text, useStyleConfig, useBreakpointValue, IconButton, Drawer, DrawerBody, DrawerHeader, DrawerOverlay, DrawerContent, DrawerCloseButton, useDisclosure, Container, Menu, MenuButton, MenuList, MenuItem, useToken } from "@chakra-ui/react";
|
||||
import { ChevronLeftIcon, ChevronRightIcon, HamburgerIcon, ViewIcon, SettingsIcon, DragHandleIcon } from "@chakra-ui/icons";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useNavigate, useLocation, Routes, Route } from "react-router-dom";
|
||||
import { SongList } from "./components/SongList";
|
||||
import { PlaylistManager } from "./components/PlaylistManager";
|
||||
import { SongDetails } from "./components/SongDetails";
|
||||
import { Configuration } from "./pages/Configuration";
|
||||
import { useXmlParser } from "./hooks/useXmlParser";
|
||||
import { exportToXml } from "./services/xmlService";
|
||||
import { api } from "./services/api";
|
||||
@ -430,157 +431,140 @@ export default function RekordboxReader() {
|
||||
fontSize="20px"
|
||||
/>
|
||||
)}
|
||||
<Heading size={isMobile ? "sm" : "md"}>Rekordbox Reader</Heading>
|
||||
|
||||
{/* Desktop Actions */}
|
||||
{!isMobile && (
|
||||
<Flex gap={2} align="center" ml="auto">
|
||||
<StyledFileInput />
|
||||
{songs.length > 0 && (
|
||||
<Button onClick={handleExport} size="sm" width="auto">
|
||||
Export XML
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
<Heading
|
||||
size={isMobile ? "sm" : "md"}
|
||||
cursor="pointer"
|
||||
onClick={() => navigate('/')}
|
||||
_hover={{ color: 'blue.300' }}
|
||||
transition="color 0.2s"
|
||||
>
|
||||
Rekordbox Reader
|
||||
</Heading>
|
||||
|
||||
{/* Mobile Actions */}
|
||||
{isMobile && (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
icon={<SettingsIcon />}
|
||||
variant="ghost"
|
||||
ml="auto"
|
||||
size="md"
|
||||
color="gray.300"
|
||||
_hover={{ color: "white", bg: "whiteAlpha.200" }}
|
||||
/>
|
||||
<MenuList bg="gray.800" borderColor="gray.700">
|
||||
<MenuItem
|
||||
bg="gray.800"
|
||||
_hover={{ bg: "gray.700" }}
|
||||
>
|
||||
<StyledFileInput isMobile />
|
||||
</MenuItem>
|
||||
{songs.length > 0 && (
|
||||
<MenuItem
|
||||
bg="gray.800"
|
||||
_hover={{ bg: "gray.700" }}
|
||||
onClick={handleExport}
|
||||
color="gray.100"
|
||||
>
|
||||
Export XML
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
{/* Configuration Button */}
|
||||
<IconButton
|
||||
icon={<SettingsIcon />}
|
||||
aria-label="Configuration"
|
||||
variant="ghost"
|
||||
ml="auto"
|
||||
color="gray.300"
|
||||
_hover={{ color: "white", bg: "whiteAlpha.200" }}
|
||||
onClick={() => navigate('/config')}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{/* Main Content */}
|
||||
<Flex flex={1} overflow="hidden" w="full">
|
||||
{/* Sidebar - Desktop */}
|
||||
{!isMobile && (
|
||||
<Box
|
||||
position="relative"
|
||||
w={`${sidebarWidth}px`}
|
||||
minW={`${sidebarWidth}px`}
|
||||
p={4}
|
||||
borderRight="1px"
|
||||
borderColor="gray.700"
|
||||
overflowY="auto"
|
||||
bg="gray.900"
|
||||
>
|
||||
{playlistManager}
|
||||
<ResizeHandle onMouseDown={handleResizeStart} />
|
||||
</Box>
|
||||
)}
|
||||
<Box flex={1} overflow="hidden">
|
||||
<Routes>
|
||||
<Route path="/config" element={<Configuration />} />
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<Flex flex={1} h="100%" overflow="hidden" w="full">
|
||||
{/* Sidebar - Desktop */}
|
||||
{!isMobile && (
|
||||
<Box
|
||||
position="relative"
|
||||
w={`${sidebarWidth}px`}
|
||||
minW={`${sidebarWidth}px`}
|
||||
p={4}
|
||||
borderRight="1px"
|
||||
borderColor="gray.700"
|
||||
overflowY="auto"
|
||||
bg="gray.900"
|
||||
>
|
||||
{playlistManager}
|
||||
<ResizeHandle onMouseDown={handleResizeStart} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Sidebar - Mobile */}
|
||||
<Drawer
|
||||
isOpen={isOpen}
|
||||
placement="left"
|
||||
onClose={onClose}
|
||||
size="full"
|
||||
>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent bg="gray.900" p={0}>
|
||||
<DrawerHeader
|
||||
borderBottomWidth="1px"
|
||||
bg="gray.800"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
px={2}
|
||||
py={2}
|
||||
>
|
||||
<Text>Playlists</Text>
|
||||
<IconButton
|
||||
aria-label="Close menu"
|
||||
icon={<ChevronLeftIcon />}
|
||||
onClick={onClose}
|
||||
variant="ghost"
|
||||
ml="auto"
|
||||
color="blue.400"
|
||||
_hover={{ color: "blue.300", bg: "whiteAlpha.200" }}
|
||||
/>
|
||||
</DrawerHeader>
|
||||
<DrawerBody p={2}>
|
||||
{playlistManager}
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{/* Sidebar - Mobile */}
|
||||
<Drawer
|
||||
isOpen={isOpen}
|
||||
placement="left"
|
||||
onClose={onClose}
|
||||
size="full"
|
||||
>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent bg="gray.900" p={0}>
|
||||
<DrawerHeader
|
||||
borderBottomWidth="1px"
|
||||
bg="gray.800"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
px={2}
|
||||
py={2}
|
||||
>
|
||||
<Text>Playlists</Text>
|
||||
<IconButton
|
||||
aria-label="Close menu"
|
||||
icon={<ChevronLeftIcon />}
|
||||
onClick={onClose}
|
||||
variant="ghost"
|
||||
ml="auto"
|
||||
color="blue.400"
|
||||
_hover={{ color: "blue.300", bg: "whiteAlpha.200" }}
|
||||
/>
|
||||
</DrawerHeader>
|
||||
<DrawerBody p={2}>
|
||||
{playlistManager}
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<Flex
|
||||
flex={1}
|
||||
gap={4}
|
||||
p={isMobile ? 2 : 4}
|
||||
overflowY="hidden"
|
||||
w="full"
|
||||
>
|
||||
{/* Song List */}
|
||||
<Box flex={1} overflowY="auto" w="full">
|
||||
<SongList
|
||||
songs={displayedSongs}
|
||||
onAddToPlaylist={handleAddSongsToPlaylist}
|
||||
onRemoveFromPlaylist={handleRemoveFromPlaylist}
|
||||
playlists={playlists}
|
||||
onSongSelect={setSelectedSong}
|
||||
selectedSongId={selectedSong?.id || null}
|
||||
currentPlaylist={currentPlaylist}
|
||||
/>
|
||||
</Box>
|
||||
{/* Main Content Area */}
|
||||
<Flex
|
||||
flex={1}
|
||||
gap={4}
|
||||
p={isMobile ? 2 : 4}
|
||||
h="100%"
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* Song List */}
|
||||
<Box flex={1} overflowY="auto" minH={0}>
|
||||
<SongList
|
||||
songs={displayedSongs}
|
||||
onAddToPlaylist={handleAddSongsToPlaylist}
|
||||
onRemoveFromPlaylist={handleRemoveFromPlaylist}
|
||||
playlists={playlists}
|
||||
onSongSelect={setSelectedSong}
|
||||
selectedSongId={selectedSong?.id || null}
|
||||
currentPlaylist={currentPlaylist}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Details Panel */}
|
||||
{!isMobile && (
|
||||
<Box
|
||||
w="350px"
|
||||
minW="350px"
|
||||
p={4}
|
||||
borderLeft="1px"
|
||||
borderColor="gray.700"
|
||||
overflowY="auto"
|
||||
bg="gray.900"
|
||||
sx={{
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'gray.900',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: 'gray.700',
|
||||
borderRadius: '8px',
|
||||
},
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
}}
|
||||
>
|
||||
<SongDetails song={selectedSong} />
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
{/* Details Panel */}
|
||||
{!isMobile && (
|
||||
<Box
|
||||
w="350px"
|
||||
minW="350px"
|
||||
p={4}
|
||||
borderLeft="1px"
|
||||
borderColor="gray.700"
|
||||
overflowY="auto"
|
||||
bg="gray.900"
|
||||
minH={0}
|
||||
sx={{
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'gray.900',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: 'gray.700',
|
||||
borderRadius: '8px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SongDetails song={selectedSong} />
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@ -107,144 +107,72 @@ export const SongList: React.FC<SongListProps> = ({
|
||||
}, [filteredSongs]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Search Bar */}
|
||||
<InputGroup mb={4}>
|
||||
<InputLeftElement pointerEvents="none">
|
||||
<Search2Icon color="gray.300" />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
placeholder="Search songs by title or artist..."
|
||||
value={searchQuery}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setSearchQuery(e.target.value)}
|
||||
bg="gray.800"
|
||||
borderColor="gray.600"
|
||||
_hover={{ borderColor: "gray.500" }}
|
||||
_focus={{ borderColor: "blue.300", boxShadow: "0 0 0 1px var(--chakra-colors-blue-300)" }}
|
||||
/>
|
||||
</InputGroup>
|
||||
<Flex direction="column" height="100%">
|
||||
{/* Sticky Header */}
|
||||
<Box
|
||||
position="sticky"
|
||||
top={0}
|
||||
bg="gray.900"
|
||||
zIndex={1}
|
||||
pb={4}
|
||||
>
|
||||
{/* Search Bar */}
|
||||
<InputGroup mb={4}>
|
||||
<InputLeftElement pointerEvents="none">
|
||||
<Search2Icon color="gray.300" />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
placeholder="Search songs by title or artist..."
|
||||
value={searchQuery}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setSearchQuery(e.target.value)}
|
||||
bg="gray.800"
|
||||
borderColor="gray.600"
|
||||
_hover={{ borderColor: "gray.500" }}
|
||||
_focus={{ borderColor: "blue.300", boxShadow: "0 0 0 1px var(--chakra-colors-blue-300)" }}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
{/* Bulk Actions Toolbar */}
|
||||
<Flex justify="space-between" align="center" mb={4} p={2} bg="gray.800" borderRadius="md">
|
||||
<HStack spacing={4}>
|
||||
<Checkbox
|
||||
isChecked={selectedSongs.size === filteredSongs.length}
|
||||
isIndeterminate={selectedSongs.size > 0 && selectedSongs.size < filteredSongs.length}
|
||||
onChange={toggleSelectAll}
|
||||
colorScheme="blue"
|
||||
sx={{
|
||||
'& > span:first-of-type': {
|
||||
opacity: 1,
|
||||
border: '2px solid',
|
||||
borderColor: 'gray.500'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedSongs.size === 0
|
||||
? "Select All"
|
||||
: `Selected ${selectedSongs.size} song${selectedSongs.size === 1 ? '' : 's'}`}
|
||||
</Checkbox>
|
||||
<Text color="gray.400" fontSize="sm">
|
||||
{filteredSongs.length} song{filteredSongs.length === 1 ? '' : 's'} • {formatTotalDuration(totalDuration)}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{selectedSongs.size > 0 && (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
>
|
||||
Actions
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
{allPlaylists.map((playlist) => (
|
||||
<MenuItem
|
||||
key={playlist.id}
|
||||
onClick={() => {
|
||||
handleBulkAddToPlaylist(playlist.name);
|
||||
}}
|
||||
>
|
||||
Add to {playlist.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
{currentPlaylist !== "All Songs" && onRemoveFromPlaylist && (
|
||||
<>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
color="red.300"
|
||||
onClick={() => {
|
||||
onRemoveFromPlaylist(Array.from(selectedSongs));
|
||||
setSelectedSongs(new Set());
|
||||
}}
|
||||
>
|
||||
Remove from {currentPlaylist}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{/* Song List */}
|
||||
<Flex direction="column" gap={2}>
|
||||
{filteredSongs.map((song) => (
|
||||
<Flex
|
||||
key={song.id}
|
||||
alignItems="center"
|
||||
p={2}
|
||||
pl={depth > 0 ? 4 + (depth * 4) : 2}
|
||||
borderBottom="1px"
|
||||
borderColor="gray.700"
|
||||
bg={selectedSongId === song.id ? "gray.700" : "transparent"}
|
||||
_hover={{ bg: "gray.800", cursor: "pointer" }}
|
||||
onClick={() => onSongSelect(song)}
|
||||
>
|
||||
{/* Bulk Actions Toolbar */}
|
||||
<Flex justify="space-between" align="center" p={2} bg="gray.800" borderRadius="md">
|
||||
<HStack spacing={4}>
|
||||
<Checkbox
|
||||
isChecked={selectedSongs.has(song.id)}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation();
|
||||
toggleSelection(song.id);
|
||||
isChecked={selectedSongs.size === filteredSongs.length}
|
||||
isIndeterminate={selectedSongs.size > 0 && selectedSongs.size < filteredSongs.length}
|
||||
onChange={toggleSelectAll}
|
||||
colorScheme="blue"
|
||||
sx={{
|
||||
'& > span:first-of-type': {
|
||||
opacity: 1,
|
||||
border: '2px solid',
|
||||
borderColor: 'gray.500'
|
||||
}
|
||||
}}
|
||||
mr={4}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
size={depth > 0 ? "sm" : "md"}
|
||||
/>
|
||||
<Box flex="1">
|
||||
<Text
|
||||
fontWeight="bold"
|
||||
color={selectedSongId === song.id ? "white" : "gray.100"}
|
||||
fontSize={depth > 0 ? "sm" : "md"}
|
||||
>
|
||||
{song.title}
|
||||
</Text>
|
||||
<Text
|
||||
fontSize={depth > 0 ? "xs" : "sm"}
|
||||
color={selectedSongId === song.id ? "gray.300" : "gray.500"}
|
||||
>
|
||||
{song.artist} • {formatDuration(song.totalTime)}
|
||||
</Text>
|
||||
</Box>
|
||||
>
|
||||
{selectedSongs.size === 0
|
||||
? "Select All"
|
||||
: `Selected ${selectedSongs.size} song${selectedSongs.size === 1 ? '' : 's'}`}
|
||||
</Checkbox>
|
||||
<Text color="gray.400" fontSize="sm">
|
||||
{filteredSongs.length} song{filteredSongs.length === 1 ? '' : 's'} • {formatTotalDuration(totalDuration)}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{selectedSongs.size > 0 && (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label="Options"
|
||||
icon={<ChevronDownIcon />}
|
||||
variant="ghost"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
size={depth > 0 ? "xs" : "sm"}
|
||||
ml="auto"
|
||||
/>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rightIcon={<ChevronDownIcon />}
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
>
|
||||
Actions
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
{allPlaylists.map((playlist) => (
|
||||
<MenuItem
|
||||
key={playlist.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddToPlaylist([song.id], playlist.name);
|
||||
onClick={() => {
|
||||
handleBulkAddToPlaylist(playlist.name);
|
||||
}}
|
||||
>
|
||||
Add to {playlist.name}
|
||||
@ -255,9 +183,9 @@ export const SongList: React.FC<SongListProps> = ({
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
color="red.300"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveFromPlaylist([song.id]);
|
||||
onClick={() => {
|
||||
onRemoveFromPlaylist(Array.from(selectedSongs));
|
||||
setSelectedSongs(new Set());
|
||||
}}
|
||||
>
|
||||
Remove from {currentPlaylist}
|
||||
@ -266,9 +194,92 @@ export const SongList: React.FC<SongListProps> = ({
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* Scrollable Song List */}
|
||||
<Box flex={1} overflowY="auto" mt={2}>
|
||||
<Flex direction="column" gap={2}>
|
||||
{filteredSongs.map((song) => (
|
||||
<Flex
|
||||
key={song.id}
|
||||
alignItems="center"
|
||||
p={2}
|
||||
pl={depth > 0 ? 4 + (depth * 4) : 2}
|
||||
borderBottom="1px"
|
||||
borderColor="gray.700"
|
||||
bg={selectedSongId === song.id ? "gray.700" : "transparent"}
|
||||
_hover={{ bg: "gray.800", cursor: "pointer" }}
|
||||
onClick={() => onSongSelect(song)}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={selectedSongs.has(song.id)}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation();
|
||||
toggleSelection(song.id);
|
||||
}}
|
||||
mr={4}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
size={depth > 0 ? "sm" : "md"}
|
||||
/>
|
||||
<Box flex="1">
|
||||
<Text
|
||||
fontWeight="bold"
|
||||
color={selectedSongId === song.id ? "white" : "gray.100"}
|
||||
fontSize={depth > 0 ? "sm" : "md"}
|
||||
>
|
||||
{song.title}
|
||||
</Text>
|
||||
<Text
|
||||
fontSize={depth > 0 ? "xs" : "sm"}
|
||||
color={selectedSongId === song.id ? "gray.300" : "gray.500"}
|
||||
>
|
||||
{song.artist} • {formatDuration(song.totalTime)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label="Options"
|
||||
icon={<ChevronDownIcon />}
|
||||
variant="ghost"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
size={depth > 0 ? "xs" : "sm"}
|
||||
ml="auto"
|
||||
/>
|
||||
<MenuList>
|
||||
{allPlaylists.map((playlist) => (
|
||||
<MenuItem
|
||||
key={playlist.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddToPlaylist([song.id], playlist.name);
|
||||
}}
|
||||
>
|
||||
Add to {playlist.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
{currentPlaylist !== "All Songs" && onRemoveFromPlaylist && (
|
||||
<>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
color="red.300"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveFromPlaylist([song.id]);
|
||||
}}
|
||||
>
|
||||
Remove from {currentPlaylist}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
59
packages/frontend/src/components/StyledFileInput.tsx
Normal file
59
packages/frontend/src/components/StyledFileInput.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { Box, Button, Input } from "@chakra-ui/react";
|
||||
import { useXmlParser } from "../hooks/useXmlParser";
|
||||
|
||||
interface StyledFileInputProps {
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
export const StyledFileInput: React.FC<StyledFileInputProps> = ({ isMobile = false }) => {
|
||||
const { handleFileUpload } = useXmlParser();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleClick = () => {
|
||||
inputRef.current?.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
width="auto"
|
||||
maxW={isMobile ? "100%" : "full"}
|
||||
>
|
||||
<Input
|
||||
type="file"
|
||||
accept=".xml"
|
||||
onChange={handleFileUpload}
|
||||
height="40px"
|
||||
padding="8px 12px"
|
||||
opacity="0"
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
width="100%"
|
||||
cursor="pointer"
|
||||
ref={inputRef}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<Button
|
||||
width="100%"
|
||||
height="40px"
|
||||
bg="gray.700"
|
||||
color="gray.300"
|
||||
border="2px dashed"
|
||||
borderColor="gray.600"
|
||||
_hover={{
|
||||
bg: "gray.600",
|
||||
borderColor: "gray.500"
|
||||
}}
|
||||
_active={{
|
||||
bg: "gray.500"
|
||||
}}
|
||||
onClick={handleClick}
|
||||
size={isMobile ? "sm" : "md"}
|
||||
>
|
||||
Choose XML File
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
51
packages/frontend/src/pages/Configuration.tsx
Normal file
51
packages/frontend/src/pages/Configuration.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { Box, Heading, VStack, Text, Button } from "@chakra-ui/react";
|
||||
import { useXmlParser } from "../hooks/useXmlParser";
|
||||
import { exportToXml } from "../services/xmlService";
|
||||
import { StyledFileInput } from "../components/StyledFileInput";
|
||||
|
||||
export function Configuration() {
|
||||
const { songs } = useXmlParser();
|
||||
|
||||
const handleExport = () => {
|
||||
const xmlContent = exportToXml(songs, []);
|
||||
const blob = new Blob([xmlContent], { type: "application/xml" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "rekordbox_playlists.xml";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box h="100%" bg="gray.900" p={8}>
|
||||
<VStack spacing={8} align="stretch" maxW="2xl" mx="auto">
|
||||
<Heading size="lg" mb={6}>Configuration</Heading>
|
||||
|
||||
<Box bg="gray.800" p={6} borderRadius="lg" borderWidth="1px" borderColor="gray.700">
|
||||
<Heading size="md" mb={4}>Library Management</Heading>
|
||||
<Text color="gray.400" mb={6}>
|
||||
Import your Rekordbox XML library or export your current library to XML format.
|
||||
</Text>
|
||||
|
||||
<VStack spacing={4} align="stretch">
|
||||
<Box>
|
||||
<Text fontWeight="medium" mb={2}>Import Library</Text>
|
||||
<StyledFileInput />
|
||||
</Box>
|
||||
|
||||
{songs.length > 0 && (
|
||||
<Box>
|
||||
<Text fontWeight="medium" mb={2}>Export Library</Text>
|
||||
<Button onClick={handleExport} width="full">
|
||||
Export XML
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user