Compare commits
No commits in common. "770c6065619478b60658a7103b7e39f1093abddf" and "e8bb2a4326b1542eb1eb75ae2dd7277afef89cb2" have entirely different histories.
770c606561
...
e8bb2a4326
@ -1,6 +1,6 @@
|
|||||||
import { Box, Button, Flex, Heading, Spinner, Text, useBreakpointValue, IconButton, Drawer, DrawerBody, DrawerHeader, DrawerOverlay, DrawerContent, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, VStack } from "@chakra-ui/react";
|
import { Box, Button, Flex, Heading, Spinner, Text, useBreakpointValue, IconButton, Drawer, DrawerBody, DrawerHeader, DrawerOverlay, DrawerContent, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, VStack } from "@chakra-ui/react";
|
||||||
import { ChevronLeftIcon, ChevronRightIcon, SettingsIcon } from "@chakra-ui/icons";
|
import { ChevronLeftIcon, ChevronRightIcon, SettingsIcon } from "@chakra-ui/icons";
|
||||||
import React, { useState, useRef, useEffect, useCallback } from "react";
|
import { useState, useRef, useEffect, useCallback } from "react";
|
||||||
import { useNavigate, useLocation, Routes, Route } from "react-router-dom";
|
import { useNavigate, useLocation, Routes, Route } from "react-router-dom";
|
||||||
import { PaginatedSongList } from "./components/PaginatedSongList";
|
import { PaginatedSongList } from "./components/PaginatedSongList";
|
||||||
import { PlaylistManager } from "./components/PlaylistManager";
|
import { PlaylistManager } from "./components/PlaylistManager";
|
||||||
@ -154,15 +154,13 @@ export default function RekordboxReader() {
|
|||||||
}, [currentPlaylist, playlists, navigate, xmlLoading]);
|
}, [currentPlaylist, playlists, navigate, xmlLoading]);
|
||||||
|
|
||||||
const handlePlaylistSelect = (name: string) => {
|
const handlePlaylistSelect = (name: string) => {
|
||||||
// Clear selected song immediately to prevent stale state
|
setSelectedSong(null); // Clear selected song when changing playlists
|
||||||
setSelectedSong(null);
|
|
||||||
|
|
||||||
// Navigate immediately without any delays
|
|
||||||
if (name === "All Songs") {
|
if (name === "All Songs") {
|
||||||
navigate("/", { replace: true });
|
navigate("/");
|
||||||
} else {
|
} else {
|
||||||
|
// Use encodeURIComponent to properly handle spaces and special characters
|
||||||
const encodedName = encodeURIComponent(name);
|
const encodedName = encodeURIComponent(name);
|
||||||
navigate(`/playlists/${encodedName}`, { replace: true });
|
navigate(`/playlists/${encodedName}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -50,8 +50,6 @@ const SongItem = memo<{
|
|||||||
onToggleSelection: (songId: string) => void;
|
onToggleSelection: (songId: string) => void;
|
||||||
showCheckbox: boolean;
|
showCheckbox: boolean;
|
||||||
}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox }) => {
|
}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox }) => {
|
||||||
// Memoize the formatted duration to prevent recalculation
|
|
||||||
const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]);
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
onSelect(song);
|
onSelect(song);
|
||||||
}, [onSelect, song]);
|
}, [onSelect, song]);
|
||||||
@ -91,7 +89,7 @@ const SongItem = memo<{
|
|||||||
</Box>
|
</Box>
|
||||||
<Box textAlign="right" ml={2}>
|
<Box textAlign="right" ml={2}>
|
||||||
<Text fontSize="xs" color="gray.500">
|
<Text fontSize="xs" color="gray.500">
|
||||||
{formattedDuration}
|
{formatDuration(song.totalTime || '')}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" color="gray.600">
|
<Text fontSize="xs" color="gray.600">
|
||||||
{song.averageBpm} BPM
|
{song.averageBpm} BPM
|
||||||
@ -215,7 +213,7 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
showCheckbox={selectedSongs.size > 0 || depth === 0}
|
showCheckbox={selectedSongs.size > 0 || depth === 0}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}, [songs, selectedSongs, selectedSongId, toggleSelection, depth]); // Removed handleSongSelect since it's already memoized
|
}, [songs, selectedSongs, selectedSongId, handleSongSelect, toggleSelection, depth]);
|
||||||
|
|
||||||
// Use total playlist duration if available, otherwise calculate from current songs
|
// Use total playlist duration if available, otherwise calculate from current songs
|
||||||
const totalDuration = useMemo(() => {
|
const totalDuration = useMemo(() => {
|
||||||
@ -259,7 +257,7 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
// Reset the flag after a short delay to prevent multiple triggers
|
// Reset the flag after a short delay to prevent multiple triggers
|
||||||
timeoutRef.current = setTimeout(() => {
|
timeoutRef.current = setTimeout(() => {
|
||||||
isTriggeringRef.current = false;
|
isTriggeringRef.current = false;
|
||||||
}, 100);
|
}, 1000);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -36,50 +36,27 @@ interface PlaylistManagerProps {
|
|||||||
onPlaylistMove: (playlistName: string, targetFolderName: string | null) => void;
|
onPlaylistMove: (playlistName: string, targetFolderName: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memoized button styles to prevent unnecessary re-renders
|
const getButtonStyles = (isSelected: boolean) => ({
|
||||||
const selectedButtonStyles = {
|
|
||||||
width: "100%",
|
width: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
bg: "blue.800",
|
bg: isSelected ? "blue.800" : "transparent",
|
||||||
color: "white",
|
color: isSelected ? "white" : "gray.100",
|
||||||
fontWeight: "600",
|
fontWeight: isSelected ? "600" : "normal",
|
||||||
borderRadius: "md",
|
borderRadius: "md",
|
||||||
px: 4,
|
px: 4,
|
||||||
py: 2,
|
py: 2,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
transition: "all 0.2s",
|
transition: "all 0.2s",
|
||||||
_hover: {
|
_hover: {
|
||||||
bg: "blue.600",
|
bg: isSelected ? "blue.600" : "whiteAlpha.200",
|
||||||
transform: "translateX(2px)",
|
transform: "translateX(2px)",
|
||||||
},
|
},
|
||||||
_active: {
|
_active: {
|
||||||
bg: "blue.700",
|
bg: isSelected ? "blue.700" : "whiteAlpha.300",
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const unselectedButtonStyles = {
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
bg: "transparent",
|
|
||||||
color: "gray.100",
|
|
||||||
fontWeight: "normal",
|
|
||||||
borderRadius: "md",
|
|
||||||
px: 4,
|
|
||||||
py: 2,
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all 0.2s",
|
|
||||||
_hover: {
|
|
||||||
bg: "whiteAlpha.200",
|
|
||||||
transform: "translateX(2px)",
|
|
||||||
},
|
|
||||||
_active: {
|
|
||||||
bg: "whiteAlpha.300",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
interface PlaylistItemProps {
|
interface PlaylistItemProps {
|
||||||
node: PlaylistNode;
|
node: PlaylistNode;
|
||||||
@ -91,7 +68,7 @@ interface PlaylistItemProps {
|
|||||||
allFolders: { name: string }[];
|
allFolders: { name: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
|
const PlaylistItem: React.FC<PlaylistItemProps> = ({
|
||||||
node,
|
node,
|
||||||
level,
|
level,
|
||||||
selectedItem,
|
selectedItem,
|
||||||
@ -101,15 +78,6 @@ const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
|
|||||||
allFolders,
|
allFolders,
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
// Memoize click handlers to prevent recreation
|
|
||||||
const handlePlaylistClick = useCallback(() => {
|
|
||||||
onPlaylistSelect(node.name);
|
|
||||||
}, [onPlaylistSelect, node.name]);
|
|
||||||
|
|
||||||
const handleFolderToggle = useCallback(() => {
|
|
||||||
setIsOpen(prev => !prev);
|
|
||||||
}, []);
|
|
||||||
const indent = level * 10; // Reverted back to 10px per level
|
const indent = level * 10; // Reverted back to 10px per level
|
||||||
|
|
||||||
if (node.type === 'folder') {
|
if (node.type === 'folder') {
|
||||||
@ -118,8 +86,8 @@ const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
|
|||||||
<Flex align="center" gap={1}>
|
<Flex align="center" gap={1}>
|
||||||
<Button
|
<Button
|
||||||
flex={1}
|
flex={1}
|
||||||
{...unselectedButtonStyles}
|
{...getButtonStyles(false)}
|
||||||
onClick={handleFolderToggle}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
ml={indent}
|
ml={indent}
|
||||||
pl={level > 0 ? 6 : 4} // Add extra padding for nested items
|
pl={level > 0 ? 6 : 4} // Add extra padding for nested items
|
||||||
justifyContent="flex-start"
|
justifyContent="flex-start"
|
||||||
@ -186,8 +154,8 @@ const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
|
|||||||
<Flex align="center" gap={0}>
|
<Flex align="center" gap={0}>
|
||||||
<Button
|
<Button
|
||||||
flex="1 1 auto"
|
flex="1 1 auto"
|
||||||
{...(selectedItem === node.name ? selectedButtonStyles : unselectedButtonStyles)}
|
{...getButtonStyles(selectedItem === node.name)}
|
||||||
onClick={handlePlaylistClick}
|
onClick={() => onPlaylistSelect(node.name)}
|
||||||
ml={indent}
|
ml={indent}
|
||||||
pl={level > 0 ? 6 : 4} // Add extra padding for nested items
|
pl={level > 0 ? 6 : 4} // Add extra padding for nested items
|
||||||
borderRightRadius={0}
|
borderRightRadius={0}
|
||||||
@ -269,7 +237,7 @@ const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
|
|||||||
</Menu>
|
</Menu>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
|
export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
|
||||||
playlists,
|
playlists,
|
||||||
@ -321,7 +289,7 @@ export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
|
|||||||
<Box>
|
<Box>
|
||||||
<VStack spacing={2} align="stretch" mb={4}>
|
<VStack spacing={2} align="stretch" mb={4}>
|
||||||
<Button
|
<Button
|
||||||
{...(selectedItem === null ? selectedButtonStyles : unselectedButtonStyles)}
|
{...getButtonStyles(selectedItem === null)}
|
||||||
onClick={() => onPlaylistSelect(null)}
|
onClick={() => onPlaylistSelect(null)}
|
||||||
>
|
>
|
||||||
All Songs
|
All Songs
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { api, type SongsResponse } from '../services/api';
|
import { api, type SongsResponse } from '../services/api';
|
||||||
import type { Song } from '../types/interfaces';
|
import type { Song } from '../types/interfaces';
|
||||||
|
|
||||||
@ -125,12 +125,9 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
|
|||||||
setIsInitialLoad(true);
|
setIsInitialLoad(true);
|
||||||
}, [initialSearch, cleanup]);
|
}, [initialSearch, cleanup]);
|
||||||
|
|
||||||
// Initial load - only run once when the hook is first created
|
// Initial load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only load if we haven't loaded anything yet
|
|
||||||
if (songs.length === 0 && !loading) {
|
|
||||||
loadPage(1);
|
loadPage(1);
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup on unmount
|
// Cleanup on unmount
|
||||||
return () => {
|
return () => {
|
||||||
@ -138,27 +135,26 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle playlist changes - optimized for immediate response
|
// Handle playlist changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (previousPlaylistRef.current !== playlistName) {
|
if (!isInitialLoad && previousPlaylistRef.current !== playlistName) {
|
||||||
// Update refs immediately
|
// Update refs
|
||||||
currentPlaylistRef.current = playlistName;
|
currentPlaylistRef.current = playlistName;
|
||||||
currentSearchQueryRef.current = searchQuery;
|
currentSearchQueryRef.current = searchQuery;
|
||||||
previousPlaylistRef.current = playlistName;
|
previousPlaylistRef.current = playlistName;
|
||||||
|
|
||||||
// Batch all state updates together to reduce re-renders
|
// Clear songs for new playlist to replace them
|
||||||
React.startTransition(() => {
|
|
||||||
setSongs([]);
|
setSongs([]);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
setSearchQuery(initialSearch);
|
setSearchQuery(initialSearch);
|
||||||
setError(null);
|
setError(null);
|
||||||
});
|
// Use setTimeout to avoid the dependency issue
|
||||||
|
setTimeout(() => {
|
||||||
// Load immediately
|
|
||||||
loadPage(1, initialSearch, playlistName);
|
loadPage(1, initialSearch, playlistName);
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
}, [playlistName, initialSearch, loadPage]);
|
}, [playlistName, isInitialLoad, initialSearch, loadPage]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
songs,
|
songs,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user