diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx
index 46376bb..2d02c4d 100644
--- a/packages/frontend/src/App.tsx
+++ b/packages/frontend/src/App.tsx
@@ -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"
/>
)}
- Rekordbox Reader
-
- {/* Desktop Actions */}
- {!isMobile && (
-
-
- {songs.length > 0 && (
-
- )}
-
- )}
+ navigate('/')}
+ _hover={{ color: 'blue.300' }}
+ transition="color 0.2s"
+ >
+ Rekordbox Reader
+
- {/* Mobile Actions */}
- {isMobile && (
-
- )}
+ {/* Configuration Button */}
+ }
+ aria-label="Configuration"
+ variant="ghost"
+ ml="auto"
+ color="gray.300"
+ _hover={{ color: "white", bg: "whiteAlpha.200" }}
+ onClick={() => navigate('/config')}
+ />
{/* Main Content */}
-
- {/* Sidebar - Desktop */}
- {!isMobile && (
-
- {playlistManager}
-
-
- )}
+
+
+ } />
+
+ {/* Sidebar - Desktop */}
+ {!isMobile && (
+
+ {playlistManager}
+
+
+ )}
- {/* Sidebar - Mobile */}
-
-
-
-
- Playlists
- }
- onClick={onClose}
- variant="ghost"
- ml="auto"
- color="blue.400"
- _hover={{ color: "blue.300", bg: "whiteAlpha.200" }}
- />
-
-
- {playlistManager}
-
-
-
+ {/* Sidebar - Mobile */}
+
+
+
+
+ Playlists
+ }
+ onClick={onClose}
+ variant="ghost"
+ ml="auto"
+ color="blue.400"
+ _hover={{ color: "blue.300", bg: "whiteAlpha.200" }}
+ />
+
+
+ {playlistManager}
+
+
+
- {/* Main Content Area */}
-
- {/* Song List */}
-
-
-
+ {/* Main Content Area */}
+
+ {/* Song List */}
+
+
+
- {/* Details Panel */}
- {!isMobile && (
-
-
-
- )}
-
-
+ {/* Details Panel */}
+ {!isMobile && (
+
+
+
+ )}
+
+
+ }
+ />
+
+
);
diff --git a/packages/frontend/src/components/SongList.tsx b/packages/frontend/src/components/SongList.tsx
index 038e8d4..546cb04 100644
--- a/packages/frontend/src/components/SongList.tsx
+++ b/packages/frontend/src/components/SongList.tsx
@@ -107,144 +107,72 @@ export const SongList: React.FC = ({
}, [filteredSongs]);
return (
-
- {/* Search Bar */}
-
-
-
-
- ) => 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)" }}
- />
-
+
+ {/* Sticky Header */}
+
+ {/* Search Bar */}
+
+
+
+
+ ) => 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)" }}
+ />
+
- {/* Bulk Actions Toolbar */}
-
-
- 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'}`}
-
-
- {filteredSongs.length} song{filteredSongs.length === 1 ? '' : 's'} • {formatTotalDuration(totalDuration)}
-
-
-
- {selectedSongs.size > 0 && (
-
- )}
-
-
- {/* Song List */}
-
- {filteredSongs.map((song) => (
- 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 */}
+
+
) => {
- 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"}
- />
-
- 0 ? "sm" : "md"}
- >
- {song.title}
-
- 0 ? "xs" : "sm"}
- color={selectedSongId === song.id ? "gray.300" : "gray.500"}
- >
- {song.artist} • {formatDuration(song.totalTime)}
-
-
+ >
+ {selectedSongs.size === 0
+ ? "Select All"
+ : `Selected ${selectedSongs.size} song${selectedSongs.size === 1 ? '' : 's'}`}
+
+
+ {filteredSongs.length} song{filteredSongs.length === 1 ? '' : 's'} • {formatTotalDuration(totalDuration)}
+
+
+
+ {selectedSongs.size > 0 && (
-
- ))}
-
-
+ )}
+
+
+
+ {/* Scrollable Song List */}
+
+
+ {filteredSongs.map((song) => (
+ 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)}
+ >
+ ) => {
+ e.stopPropagation();
+ toggleSelection(song.id);
+ }}
+ mr={4}
+ onClick={(e) => e.stopPropagation()}
+ size={depth > 0 ? "sm" : "md"}
+ />
+
+ 0 ? "sm" : "md"}
+ >
+ {song.title}
+
+ 0 ? "xs" : "sm"}
+ color={selectedSongId === song.id ? "gray.300" : "gray.500"}
+ >
+ {song.artist} • {formatDuration(song.totalTime)}
+
+
+
+
+ ))}
+
+
+
);
};
\ No newline at end of file
diff --git a/packages/frontend/src/components/StyledFileInput.tsx b/packages/frontend/src/components/StyledFileInput.tsx
new file mode 100644
index 0000000..d4afc06
--- /dev/null
+++ b/packages/frontend/src/components/StyledFileInput.tsx
@@ -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 = ({ isMobile = false }) => {
+ const { handleFileUpload } = useXmlParser();
+ const inputRef = useRef(null);
+
+ const handleClick = () => {
+ inputRef.current?.click();
+ };
+
+ return (
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/packages/frontend/src/pages/Configuration.tsx b/packages/frontend/src/pages/Configuration.tsx
new file mode 100644
index 0000000..3df2d9c
--- /dev/null
+++ b/packages/frontend/src/pages/Configuration.tsx
@@ -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 (
+
+
+ Configuration
+
+
+ Library Management
+
+ Import your Rekordbox XML library or export your current library to XML format.
+
+
+
+
+ Import Library
+
+
+
+ {songs.length > 0 && (
+
+ Export Library
+
+
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file