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