From db4408b953ddc8b1f5f02a56a601c173dfa6cbab Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 25 Apr 2025 10:29:24 +0200 Subject: [PATCH] Added config page! --- packages/frontend/src/App.tsx | 274 ++++++++--------- packages/frontend/src/components/SongList.tsx | 285 +++++++++--------- .../src/components/StyledFileInput.tsx | 59 ++++ packages/frontend/src/pages/Configuration.tsx | 51 ++++ 4 files changed, 387 insertions(+), 282 deletions(-) create mode 100644 packages/frontend/src/components/StyledFileInput.tsx create mode 100644 packages/frontend/src/pages/Configuration.tsx 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 && ( - - } - variant="ghost" - ml="auto" - size="md" - color="gray.300" - _hover={{ color: "white", bg: "whiteAlpha.200" }} - /> - - - - - {songs.length > 0 && ( - - Export XML - - )} - - - )} + {/* 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 && ( - - } - size="sm" - colorScheme="blue" - > - Actions - - - {allPlaylists.map((playlist) => ( - { - handleBulkAddToPlaylist(playlist.name); - }} - > - Add to {playlist.name} - - ))} - {currentPlaylist !== "All Songs" && onRemoveFromPlaylist && ( - <> - - { - onRemoveFromPlaylist(Array.from(selectedSongs)); - setSelectedSongs(new Set()); - }} - > - Remove from {currentPlaylist} - - - )} - - - )} - - - {/* 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 && ( - } - variant="ghost" - onClick={(e) => e.stopPropagation()} - size={depth > 0 ? "xs" : "sm"} - ml="auto" - /> + } + size="sm" + colorScheme="blue" + > + Actions + {allPlaylists.map((playlist) => ( { - e.stopPropagation(); - onAddToPlaylist([song.id], playlist.name); + onClick={() => { + handleBulkAddToPlaylist(playlist.name); }} > Add to {playlist.name} @@ -255,9 +183,9 @@ export const SongList: React.FC = ({ { - 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 = ({ )} - - ))} - - + )} + + + + {/* 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)} + + + + } + variant="ghost" + onClick={(e) => e.stopPropagation()} + size={depth > 0 ? "xs" : "sm"} + ml="auto" + /> + + {allPlaylists.map((playlist) => ( + { + e.stopPropagation(); + onAddToPlaylist([song.id], playlist.name); + }} + > + Add to {playlist.name} + + ))} + {currentPlaylist !== "All Songs" && onRemoveFromPlaylist && ( + <> + + { + e.stopPropagation(); + onRemoveFromPlaylist([song.id]); + }} + > + Remove from {currentPlaylist} + + + )} + + + + ))} + + + ); }; \ 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