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 { SongList } from "./components/SongList"; import { PlaylistManager } from "./components/PlaylistManager"; import { SongDetails } from "./components/SongDetails"; import { useXmlParser } from "./hooks/useXmlParser"; import { exportToXml } from "./services/xmlService"; import { api } from "./services/api"; import type { Song, PlaylistNode } from "./types/interfaces"; import { v4 as uuidv4 } from "uuid"; import "./App.css"; const StyledFileInput = ({ isMobile = false }) => { const { handleFileUpload } = useXmlParser(); const inputRef = useRef(null); const handleClick = () => { inputRef.current?.click(); }; return ( ); }; const ResizeHandle = ({ onMouseDown }: { onMouseDown: (e: React.MouseEvent) => void }) => ( ); const findPlaylistByName = (playlists: PlaylistNode[], name: string): PlaylistNode | null => { for (const playlist of playlists) { if (playlist.name === name) return playlist; if (playlist.type === 'folder' && playlist.children) { const found = findPlaylistByName(playlist.children, name); if (found) return found; } } return null; }; const getAllPlaylistTracks = (node: PlaylistNode): string[] => { if (!node.type || node.type === 'playlist') { // Handle both old and new playlist formats return node.tracks || []; } if (node.type === 'folder' && node.children) { return node.children.flatMap(child => getAllPlaylistTracks(child)); } return []; }; export default function RekordboxReader() { const { songs, playlists, setPlaylists, loading } = useXmlParser(); const [selectedSong, setSelectedSong] = useState(null); const navigate = useNavigate(); const location = useLocation(); const initialLoadDone = useRef(false); const mobileFileInputRef = useRef(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" const currentPlaylist = location.pathname === "/" ? "All Songs" : decodeURIComponent(location.pathname.slice(1)); useEffect(() => { // Only run this check after the initial data load if (!loading && playlists.length > 0) { initialLoadDone.current = true; } // If we've loaded the data and the playlist doesn't exist if (initialLoadDone.current && currentPlaylist !== "All Songs" && !findPlaylistByName(playlists, currentPlaylist)) { navigate("/", { replace: true }); } }, [currentPlaylist, playlists, navigate, loading]); const handlePlaylistSelect = (name: string) => { setSelectedSong(null); // Clear selected song when changing playlists if (name === "All Songs") { navigate("/"); } else { // Use encodeURIComponent to properly handle spaces and special characters const encodedName = encodeURIComponent(name); navigate(`/${encodedName}`); } }; const handleCreatePlaylist = async (name: string) => { const newPlaylist: PlaylistNode = { id: uuidv4(), name, type: 'playlist', tracks: [], children: undefined }; const updatedPlaylists = [...playlists, newPlaylist]; const savedPlaylists = await api.savePlaylists(updatedPlaylists); setPlaylists(savedPlaylists); handlePlaylistSelect(name); // Navigate to the new playlist }; const handleAddSongsToPlaylist = async (songIds: string[], playlistName: string) => { const updatedPlaylists = playlists.map(node => { if (node.name === playlistName && node.type === 'playlist') { return { ...node, tracks: [...new Set([...(node.tracks || []), ...songIds])] }; } if (node.type === 'folder' && node.children) { return { ...node, children: node.children.map(child => { if (child.name === playlistName && child.type === 'playlist') { return { ...child, tracks: [...new Set([...(child.tracks || []), ...songIds])] }; } return child; }) }; } return node; }); const savedPlaylists = await api.savePlaylists(updatedPlaylists); setPlaylists(savedPlaylists); }; const handleRemoveFromPlaylist = async (songIds: string[]) => { if (currentPlaylist === "All Songs") return; const updatedPlaylists = playlists.map(node => { if (node.name === currentPlaylist && node.type === 'playlist') { return { ...node, tracks: (node.tracks || []).filter(id => !songIds.includes(id)) }; } if (node.type === 'folder' && node.children) { return { ...node, children: node.children.map(child => { if (child.name === currentPlaylist && child.type === 'playlist') { return { ...child, tracks: (child.tracks || []).filter(id => !songIds.includes(id)) }; } return child; }) }; } return node; }); const savedPlaylists = await api.savePlaylists(updatedPlaylists); setPlaylists(savedPlaylists); }; const handleExport = () => { const xmlContent = exportToXml(songs, playlists); 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); }; 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" ? songs : songs.filter((song: Song) => { // Try to find the playlist either in nested structure or at root level const playlist = findPlaylistByName(playlists, currentPlaylist) || playlists.find(p => p.name === currentPlaylist); if (!playlist) return false; // Use getAllPlaylistTracks for both nested and root playlists const playlistTracks = getAllPlaylistTracks(playlist); return playlistTracks.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) { return ( Loading your library... {currentPlaylist !== "All Songs" && ( Navigating to playlist: {currentPlaylist} )} ); } const playlistManager = ( { handlePlaylistSelect(name || "All Songs"); if (isMobile) onClose(); }} onPlaylistDelete={handlePlaylistDelete} /> ); return ( {/* Header */} {isMobile && ( } onClick={onOpen} variant="solid" colorScheme="blue" size="md" fontSize="20px" /> )} Rekordbox Reader {/* Desktop Actions */} {!isMobile && ( {songs.length > 0 && ( )} )} {/* Mobile Actions */} {isMobile && ( } variant="ghost" ml="auto" size="md" color="gray.300" _hover={{ color: "white", bg: "whiteAlpha.200" }} /> {songs.length > 0 && ( Export XML )} )} {/* Main Content */} {/* Sidebar - Desktop */} {!isMobile && ( {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 */} {/* Details Panel */} {!isMobile && ( )} ); }