Compare commits

..

No commits in common. "f82cb84397946564c293b947c4cb8c41a2ae43ed" and "8daf0cd526da004b828c32d711283a3bfe4d124d" have entirely different histories.

4 changed files with 130 additions and 382 deletions

View File

@ -1,19 +1,7 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #root {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
#root { #root {
max-width: 1280px; max-width: 1280px;
margin: 0;
padding: 1rem;
} }
.logo { .logo {

View File

@ -1,5 +1,4 @@
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 } from "@chakra-ui/react";
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 } from "react-router-dom";
import { SongList } from "./components/SongList"; import { SongList } from "./components/SongList";
@ -11,7 +10,7 @@ import { api } from "./services/api";
import { Song } from "./types/interfaces"; import { Song } from "./types/interfaces";
import "./App.css"; import "./App.css";
const StyledFileInput = ({ isMobile = false }) => { const StyledFileInput = () => {
const { handleFileUpload } = useXmlParser(); const { handleFileUpload } = useXmlParser();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@ -23,7 +22,7 @@ const StyledFileInput = ({ isMobile = false }) => {
<Box <Box
position="relative" position="relative"
width="auto" width="auto"
maxW={isMobile ? "100%" : "300px"} maxW="300px"
> >
<Input <Input
type="file" type="file"
@ -55,7 +54,6 @@ const StyledFileInput = ({ isMobile = false }) => {
bg: "gray.500" bg: "gray.500"
}} }}
onClick={handleClick} onClick={handleClick}
size={isMobile ? "sm" : "md"}
> >
Choose XML File Choose XML File
</Button> </Button>
@ -63,48 +61,12 @@ const StyledFileInput = ({ isMobile = false }) => {
); );
}; };
const ResizeHandle = ({ onMouseDown }: { onMouseDown: (e: React.MouseEvent) => void }) => (
<Box
position="absolute"
right="-4px"
top={0}
bottom={0}
width="8px"
cursor="col-resize"
zIndex={1}
_hover={{
'&::after': {
bg: 'blue.500',
}
}}
onMouseDown={onMouseDown}
>
<Box
position="absolute"
left="3px"
top={0}
bottom={0}
width="2px"
bg="gray.600"
transition="background-color 0.2s"
_hover={{ bg: 'blue.500' }}
/>
</Box>
);
export default function RekordboxReader() { export default function RekordboxReader() {
const { songs, playlists, setPlaylists, loading } = useXmlParser(); const { songs, playlists, setPlaylists, loading } = useXmlParser();
const [selectedSong, setSelectedSong] = useState<Song | null>(null); const [selectedSong, setSelectedSong] = useState<Song | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const initialLoadDone = useRef(false); const initialLoadDone = useRef(false);
const mobileFileInputRef = useRef<HTMLInputElement>(null);
const { isOpen, onOpen, onClose } = useDisclosure();
const isMobile = useBreakpointValue({ base: true, md: false });
const [sidebarWidth, setSidebarWidth] = useState(400);
const [isResizing, setIsResizing] = useState(false);
const resizeRef = useRef<{ startX: number; startWidth: number } | null>(null);
// Get the current playlist from URL or default to "All Songs" // Get the current playlist from URL or default to "All Songs"
const currentPlaylist = location.pathname === "/" const currentPlaylist = location.pathname === "/"
@ -178,54 +140,12 @@ export default function RekordboxReader() {
document.body.removeChild(a); document.body.removeChild(a);
}; };
const handlePlaylistDelete = async (name: string) => {
const updatedPlaylists = playlists.filter(p => p.name !== name);
const savedPlaylists = await api.savePlaylists(updatedPlaylists);
setPlaylists(savedPlaylists);
if (currentPlaylist === name) {
navigate("/"); // Navigate back to All Songs if the current playlist is deleted
}
};
const displayedSongs = currentPlaylist === "All Songs" const displayedSongs = currentPlaylist === "All Songs"
? songs ? songs
: songs.filter((song) => : songs.filter((song) =>
playlists.find((p) => p.name === currentPlaylist)?.tracks.includes(song.id) playlists.find((p) => p.name === currentPlaylist)?.tracks.includes(song.id)
); );
const handleResizeStart = (e: React.MouseEvent) => {
e.preventDefault();
setIsResizing(true);
resizeRef.current = {
startX: e.pageX,
startWidth: sidebarWidth,
};
};
const handleResizeMove = (e: MouseEvent) => {
if (!isResizing || !resizeRef.current) return;
const delta = e.pageX - resizeRef.current.startX;
const newWidth = Math.max(300, Math.min(800, resizeRef.current.startWidth + delta));
setSidebarWidth(newWidth);
};
const handleResizeEnd = () => {
setIsResizing(false);
resizeRef.current = null;
};
useEffect(() => {
if (isResizing) {
window.addEventListener('mousemove', handleResizeMove);
window.addEventListener('mouseup', handleResizeEnd);
}
return () => {
window.removeEventListener('mousemove', handleResizeMove);
window.removeEventListener('mouseup', handleResizeEnd);
};
}, [isResizing]);
if (loading) { if (loading) {
return ( return (
<Flex height="100vh" align="center" justify="center" direction="column" gap={4}> <Flex height="100vh" align="center" justify="center" direction="column" gap={4}>
@ -240,59 +160,11 @@ export default function RekordboxReader() {
); );
} }
const playlistManager = (
<PlaylistManager
playlists={playlists}
selectedItem={currentPlaylist}
onPlaylistCreate={handleCreatePlaylist}
onPlaylistSelect={(name) => {
handlePlaylistSelect(name || "All Songs");
if (isMobile) onClose();
}}
onPlaylistDelete={handlePlaylistDelete}
/>
);
return ( return (
<Box <Box>
position="fixed" <Flex direction="column" gap={4} mb={6}>
top={0} <Heading size="md" mb={2}>Rekordbox Reader</Heading>
left={0} <Flex gap={4} align="center">
right={0}
bottom={0}
overflow="hidden"
margin={0}
padding={0}
userSelect={isResizing ? 'none' : 'auto'}
>
<Flex direction="column" h="100%" w="100%">
{/* Header */}
<Flex
px={isMobile ? 2 : 4}
py={2}
bg="gray.800"
borderBottom="1px"
borderColor="gray.700"
align="center"
gap={2}
w="full"
>
{isMobile && (
<IconButton
aria-label="Open menu"
icon={<ChevronRightIcon />}
onClick={onOpen}
variant="solid"
colorScheme="blue"
size="md"
fontSize="20px"
/>
)}
<Heading size={isMobile ? "sm" : "md"}>Rekordbox Reader</Heading>
{/* Desktop Actions */}
{!isMobile && (
<Flex gap={2} align="center" ml="auto">
<StyledFileInput /> <StyledFileInput />
{songs.length > 0 && ( {songs.length > 0 && (
<Button onClick={handleExport} size="sm" width="auto"> <Button onClick={handleExport} size="sm" width="auto">
@ -300,105 +172,17 @@ export default function RekordboxReader() {
</Button> </Button>
)} )}
</Flex> </Flex>
)}
{/* 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>
)}
</Flex> </Flex>
<Flex gap={4} alignItems="start">
{/* Main Content */} <Box w="200px">
<Flex flex={1} overflow="hidden" w="full"> <PlaylistManager
{/* Sidebar - Desktop */} playlists={playlists}
{!isMobile && ( selectedItem={currentPlaylist}
<Box onPlaylistCreate={handleCreatePlaylist}
position="relative" onPlaylistSelect={handlePlaylistSelect}
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> </Box>
<DrawerBody p={2}> <Box flex={1}>
{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 <SongList
songs={displayedSongs} songs={displayedSongs}
onAddToPlaylist={handleAddSongsToPlaylist} onAddToPlaylist={handleAddSongsToPlaylist}
@ -409,36 +193,7 @@ export default function RekordboxReader() {
currentPlaylist={currentPlaylist} currentPlaylist={currentPlaylist}
/> />
</Box> </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} /> <SongDetails song={selectedSong} />
</Box>
)}
</Flex>
</Flex>
</Flex> </Flex>
</Box> </Box>
); );

View File

@ -13,13 +13,7 @@ import {
Text, Text,
useDisclosure, useDisclosure,
VStack, VStack,
Menu,
MenuButton,
MenuList,
MenuItem,
IconButton,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { ChevronDownIcon, DeleteIcon } from "@chakra-ui/icons";
import React, { useState } from "react"; import React, { useState } from "react";
import { Playlist } from "../types/Playlist"; import { Playlist } from "../types/Playlist";
@ -27,8 +21,7 @@ interface PlaylistManagerProps {
playlists: Playlist[]; playlists: Playlist[];
selectedItem: string | null; selectedItem: string | null;
onPlaylistCreate: (name: string) => void; onPlaylistCreate: (name: string) => void;
onPlaylistSelect: (name: string | null) => void; onPlaylistSelect: (id: string | null) => void;
onPlaylistDelete: (name: string) => void;
} }
const getButtonStyles = (isSelected: boolean) => ({ const getButtonStyles = (isSelected: boolean) => ({
@ -55,7 +48,6 @@ export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
selectedItem, selectedItem,
onPlaylistCreate, onPlaylistCreate,
onPlaylistSelect, onPlaylistSelect,
onPlaylistDelete,
}) => { }) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const [newPlaylistName, setNewPlaylistName] = useState(""); const [newPlaylistName, setNewPlaylistName] = useState("");
@ -78,37 +70,13 @@ export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
All Songs All Songs
</Button> </Button>
{playlists.map((playlist) => ( {playlists.map((playlist) => (
<Flex key={playlist._id} align="center" gap={1}>
<Button <Button
flex={1} key={playlist._id}
{...getButtonStyles(selectedItem === playlist.name)} {...getButtonStyles(selectedItem === playlist.name)}
onClick={() => onPlaylistSelect(playlist.name)} onClick={() => onPlaylistSelect(playlist.name)}
> >
{playlist.name} {playlist.name}
</Button> </Button>
<Menu>
<MenuButton
as={IconButton}
aria-label="Playlist options"
icon={<ChevronDownIcon />}
variant="ghost"
size="sm"
color="gray.400"
_hover={{ color: "white", bg: "whiteAlpha.200" }}
/>
<MenuList bg="gray.800" borderColor="gray.700">
<MenuItem
bg="gray.800"
color="red.300"
_hover={{ bg: "gray.700" }}
icon={<DeleteIcon />}
onClick={() => onPlaylistDelete(playlist.name)}
>
Delete Playlist
</MenuItem>
</MenuList>
</Menu>
</Flex>
))} ))}
</VStack> </VStack>

View File

@ -8,7 +8,17 @@ interface SongDetailsProps {
export const SongDetails: React.FC<SongDetailsProps> = ({ song }) => { export const SongDetails: React.FC<SongDetailsProps> = ({ song }) => {
if (!song) { if (!song) {
return ( return (
<Box h="full" p={4}> <Box
w="300px"
position="sticky"
top={4}
h="calc(100vh - 2rem)"
bg="gray.800"
p={4}
borderRadius="md"
borderLeft="1px"
borderColor="gray.700"
>
<Text color="gray.500">Select a song to view details</Text> <Text color="gray.500">Select a song to view details</Text>
</Box> </Box>
); );
@ -26,11 +36,37 @@ export const SongDetails: React.FC<SongDetailsProps> = ({ song }) => {
{ label: "Mix", value: song.mix }, { label: "Mix", value: song.mix },
{ label: "Rating", value: song.rating }, { label: "Rating", value: song.rating },
{ label: "Comments", value: song.comments }, { label: "Comments", value: song.comments },
].filter(detail => detail.value); ].filter(detail => detail.value); // Only show fields that have values
return ( return (
<Box h="full"> <Box
<VStack align="stretch" spacing={4} p={4}> w="300px"
position="sticky"
top={4}
h="calc(100vh - 2rem)"
bg="gray.800"
borderRadius="md"
borderLeft="1px"
borderColor="gray.700"
display="flex"
flexDirection="column"
>
<Box p={4} flex="1" overflowY="auto" css={{
'&::-webkit-scrollbar': {
width: '4px',
},
'&::-webkit-scrollbar-track': {
background: 'transparent',
},
'&::-webkit-scrollbar-thumb': {
background: 'var(--chakra-colors-gray-600)',
borderRadius: '2px',
},
'&::-webkit-scrollbar-thumb:hover': {
background: 'var(--chakra-colors-gray-500)',
},
}}>
<VStack align="stretch" spacing={4}>
<Box> <Box>
<Text fontSize="lg" fontWeight="bold" color="white"> <Text fontSize="lg" fontWeight="bold" color="white">
{song.title} {song.title}
@ -78,5 +114,6 @@ export const SongDetails: React.FC<SongDetailsProps> = ({ song }) => {
)} )}
</VStack> </VStack>
</Box> </Box>
</Box>
); );
}; };