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 { 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 { ChevronLeftIcon, ChevronRightIcon, HamburgerIcon, ViewIcon, SettingsIcon, DragHandleIcon } from "@chakra-ui/icons";
|
||||||
import { useState, useRef, useEffect } from "react";
|
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 { SongList } from "./components/SongList";
|
||||||
import { PlaylistManager } from "./components/PlaylistManager";
|
import { PlaylistManager } from "./components/PlaylistManager";
|
||||||
import { SongDetails } from "./components/SongDetails";
|
import { SongDetails } from "./components/SongDetails";
|
||||||
|
import { Configuration } from "./pages/Configuration";
|
||||||
import { useXmlParser } from "./hooks/useXmlParser";
|
import { useXmlParser } from "./hooks/useXmlParser";
|
||||||
import { exportToXml } from "./services/xmlService";
|
import { exportToXml } from "./services/xmlService";
|
||||||
import { api } from "./services/api";
|
import { api } from "./services/api";
|
||||||
@ -430,56 +431,36 @@ export default function RekordboxReader() {
|
|||||||
fontSize="20px"
|
fontSize="20px"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Heading size={isMobile ? "sm" : "md"}>Rekordbox Reader</Heading>
|
<Heading
|
||||||
|
size={isMobile ? "sm" : "md"}
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
_hover={{ color: 'blue.300' }}
|
||||||
|
transition="color 0.2s"
|
||||||
|
>
|
||||||
|
Rekordbox Reader
|
||||||
|
</Heading>
|
||||||
|
|
||||||
{/* Desktop Actions */}
|
{/* Configuration Button */}
|
||||||
{!isMobile && (
|
<IconButton
|
||||||
<Flex gap={2} align="center" ml="auto">
|
|
||||||
<StyledFileInput />
|
|
||||||
{songs.length > 0 && (
|
|
||||||
<Button onClick={handleExport} size="sm" width="auto">
|
|
||||||
Export XML
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Mobile Actions */}
|
|
||||||
{isMobile && (
|
|
||||||
<Menu>
|
|
||||||
<MenuButton
|
|
||||||
as={IconButton}
|
|
||||||
icon={<SettingsIcon />}
|
icon={<SettingsIcon />}
|
||||||
|
aria-label="Configuration"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
ml="auto"
|
ml="auto"
|
||||||
size="md"
|
|
||||||
color="gray.300"
|
color="gray.300"
|
||||||
_hover={{ color: "white", bg: "whiteAlpha.200" }}
|
_hover={{ color: "white", bg: "whiteAlpha.200" }}
|
||||||
|
onClick={() => navigate('/config')}
|
||||||
/>
|
/>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<Flex flex={1} overflow="hidden" w="full">
|
<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 */}
|
{/* Sidebar - Desktop */}
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<Box
|
<Box
|
||||||
@ -536,11 +517,11 @@ export default function RekordboxReader() {
|
|||||||
flex={1}
|
flex={1}
|
||||||
gap={4}
|
gap={4}
|
||||||
p={isMobile ? 2 : 4}
|
p={isMobile ? 2 : 4}
|
||||||
overflowY="hidden"
|
h="100%"
|
||||||
w="full"
|
overflow="hidden"
|
||||||
>
|
>
|
||||||
{/* Song List */}
|
{/* Song List */}
|
||||||
<Box flex={1} overflowY="auto" w="full">
|
<Box flex={1} overflowY="auto" minH={0}>
|
||||||
<SongList
|
<SongList
|
||||||
songs={displayedSongs}
|
songs={displayedSongs}
|
||||||
onAddToPlaylist={handleAddSongsToPlaylist}
|
onAddToPlaylist={handleAddSongsToPlaylist}
|
||||||
@ -562,6 +543,7 @@ export default function RekordboxReader() {
|
|||||||
borderColor="gray.700"
|
borderColor="gray.700"
|
||||||
overflowY="auto"
|
overflowY="auto"
|
||||||
bg="gray.900"
|
bg="gray.900"
|
||||||
|
minH={0}
|
||||||
sx={{
|
sx={{
|
||||||
'&::-webkit-scrollbar': {
|
'&::-webkit-scrollbar': {
|
||||||
width: '8px',
|
width: '8px',
|
||||||
@ -572,8 +554,6 @@ export default function RekordboxReader() {
|
|||||||
backgroundColor: 'gray.700',
|
backgroundColor: 'gray.700',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
},
|
},
|
||||||
overflowY: 'auto',
|
|
||||||
overflowX: 'hidden',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SongDetails song={selectedSong} />
|
<SongDetails song={selectedSong} />
|
||||||
@ -581,6 +561,10 @@ export default function RekordboxReader() {
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -107,7 +107,15 @@ export const SongList: React.FC<SongListProps> = ({
|
|||||||
}, [filteredSongs]);
|
}, [filteredSongs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Flex direction="column" height="100%">
|
||||||
|
{/* Sticky Header */}
|
||||||
|
<Box
|
||||||
|
position="sticky"
|
||||||
|
top={0}
|
||||||
|
bg="gray.900"
|
||||||
|
zIndex={1}
|
||||||
|
pb={4}
|
||||||
|
>
|
||||||
{/* Search Bar */}
|
{/* Search Bar */}
|
||||||
<InputGroup mb={4}>
|
<InputGroup mb={4}>
|
||||||
<InputLeftElement pointerEvents="none">
|
<InputLeftElement pointerEvents="none">
|
||||||
@ -125,7 +133,7 @@ export const SongList: React.FC<SongListProps> = ({
|
|||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
{/* Bulk Actions Toolbar */}
|
{/* Bulk Actions Toolbar */}
|
||||||
<Flex justify="space-between" align="center" mb={4} p={2} bg="gray.800" borderRadius="md">
|
<Flex justify="space-between" align="center" p={2} bg="gray.800" borderRadius="md">
|
||||||
<HStack spacing={4}>
|
<HStack spacing={4}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
isChecked={selectedSongs.size === filteredSongs.length}
|
isChecked={selectedSongs.size === filteredSongs.length}
|
||||||
@ -188,8 +196,10 @@ export const SongList: React.FC<SongListProps> = ({
|
|||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Song List */}
|
{/* Scrollable Song List */}
|
||||||
|
<Box flex={1} overflowY="auto" mt={2}>
|
||||||
<Flex direction="column" gap={2}>
|
<Flex direction="column" gap={2}>
|
||||||
{filteredSongs.map((song) => (
|
{filteredSongs.map((song) => (
|
||||||
<Flex
|
<Flex
|
||||||
@ -270,5 +280,6 @@ export const SongList: React.FC<SongListProps> = ({
|
|||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</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