Compare commits

...

10 Commits

Author SHA1 Message Date
Geert Rademakes
e8bb2a4326 fix: Restore playlist switching functionality - Add previousPlaylistRef to properly detect playlist changes - Fix playlist change detection logic that was broken by ref updates - Ensure playlist switching works while maintaining infinite scroll fix 2025-08-06 10:43:18 +02:00
Geert Rademakes
08de2afa0e fix: Prevent infinite scroll from loading previous playlist songs - Add refs to store current playlist and search query values - Fix stale closure issue in loadPage function - Ensure infinite scroll uses current playlist, not cached values - Fixes race condition when switching playlists and scrolling 2025-08-06 10:39:51 +02:00
Geert Rademakes
535dc16d2c fix: Prevent welcome modal from showing on initial page load - Change welcome modal to not open by default - Only show welcome modal after confirming database is empty - Move database initialization check after useDisclosure hook - Fixes premature welcome modal display on app startup 2025-08-06 10:35:17 +02:00
Geert Rademakes
743ed6a54e fix: Prevent welcome modal from showing during playlist switches - Add isDatabaseInitialized state to track actual database status - Check for songs and playlists to determine if database is initialized - Only show welcome modal when database is truly empty - Fixes issue where modal appeared when switching playlists 2025-08-06 10:33:16 +02:00
Geert Rademakes
02ae12294c feat: Show total playlist duration instead of loaded songs duration - Add totalDuration calculation in backend playlist endpoint - Update frontend to display total playlist duration - Duration now shows entire playlist length, not just loaded songs - Prevents duration from changing when scrolling through songs 2025-08-06 10:30:02 +02:00
Geert Rademakes
54bffbc25d feat: Make playlist switching less intrusive - Remove full loading screen for playlist switches - Only show full loading for initial XML parsing - Use subtle spinner in song list area for playlist loading - Improve UX by keeping interface responsive during navigation 2025-08-06 10:24:04 +02:00
Geert Rademakes
65120ff654 revert: Change indentation back to 10px per level - Revert from 16px to 10px for better visual balance - Keep visual indicators and padding improvements 2025-08-06 10:22:14 +02:00
Geert Rademakes
50f29b900e feat: Move 'Add Playlist' and 'Add Folder' buttons to the top - Position buttons right after 'All Songs' for better accessibility - Add proper spacing with mb={2} for visual separation - Improve UX by making action buttons more prominent 2025-08-06 10:21:49 +02:00
Geert Rademakes
5fb878a0b0 feat: Improve playlist hierarchy visualization - Increase indentation for nested playlists (16px per level) - Add visual indicators (gray bars) for nested items - Add extra padding for better visual separation - Make folder structure more clearly visible 2025-08-06 10:21:12 +02:00
Geert Rademakes
4f16c859db fix: Prevent infinite scroll refresh by stabilizing intersection observer - Add trigger prevention mechanism to avoid multiple API calls - Use refs to store current values and avoid stale closures - Remove observer dependencies to prevent recreation - Add proper timeout cleanup to prevent memory leaks 2025-08-06 10:13:21 +02:00
6 changed files with 216 additions and 85 deletions

View File

@ -148,6 +148,15 @@ router.get('/playlist/*', async (req: Request, res: Response) => {
const totalSongs = await Song.countDocuments(query); const totalSongs = await Song.countDocuments(query);
const totalPages = Math.ceil(totalSongs / limit); const totalPages = Math.ceil(totalSongs / limit);
// Calculate total duration for the entire playlist
const allPlaylistSongs = await Song.find({ id: { $in: trackIds } }).lean();
const totalDuration = allPlaylistSongs.reduce((total, song: any) => {
if (!song.totalTime) return total;
const totalTimeStr = String(song.totalTime);
const seconds = Math.floor(Number(totalTimeStr) / (totalTimeStr.length > 4 ? 1000 : 1));
return total + seconds;
}, 0);
// Get songs with pagination // Get songs with pagination
const songs = await Song.find(query) const songs = await Song.find(query)
.sort({ title: 1 }) .sort({ title: 1 })
@ -166,7 +175,8 @@ router.get('/playlist/*', async (req: Request, res: Response) => {
totalPages, totalPages,
hasNextPage: page < totalPages, hasNextPage: page < totalPages,
hasPrevPage: page > 1 hasPrevPage: page > 1
} },
totalDuration: totalDuration
}); });
} catch (error) { } catch (error) {
console.error('Error fetching playlist songs:', error); console.error('Error fetching playlist songs:', error);

View File

@ -8,6 +8,7 @@ import { SongDetails } from "./components/SongDetails";
import { Configuration } from "./pages/Configuration"; import { Configuration } from "./pages/Configuration";
import { useXmlParser } from "./hooks/useXmlParser"; import { useXmlParser } from "./hooks/useXmlParser";
import { usePaginatedSongs } from "./hooks/usePaginatedSongs"; import { usePaginatedSongs } from "./hooks/usePaginatedSongs";
import { formatTotalDuration } from "./utils/formatters";
import { api } from "./services/api"; import { api } from "./services/api";
import type { Song, PlaylistNode } from "./types/interfaces"; import type { Song, PlaylistNode } from "./types/interfaces";
@ -56,30 +57,40 @@ const findPlaylistByName = (playlists: PlaylistNode[], name: string): PlaylistNo
return null; return null;
}; };
const getAllPlaylistTracks = (node: PlaylistNode): string[] => { const getAllPlaylistTracks = (node: PlaylistNode): string[] => {
if (!node.type || node.type === 'playlist') { // Handle both old and new playlist formats if (!node.type || node.type === 'playlist') { // Handle both old and new playlist formats
return node.tracks || []; return node.tracks || [];
} }
if (node.type === 'folder' && node.children) { if (node.type === 'folder' && node.children) {
return node.children.flatMap(child => getAllPlaylistTracks(child)); return node.children.flatMap(child => getAllPlaylistTracks(child));
} }
return []; return [];
}; };
export default function RekordboxReader() { export default function RekordboxReader() {
const { playlists, setPlaylists, loading: xmlLoading } = useXmlParser(); const { playlists, setPlaylists, loading: xmlLoading } = useXmlParser();
const [selectedSong, setSelectedSong] = useState<Song | null>(null); const [selectedSong, setSelectedSong] = useState<Song | null>(null);
const [isDatabaseInitialized, setIsDatabaseInitialized] = useState(false);
// Memoized song selection handler to prevent unnecessary re-renders // Memoized song selection handler to prevent unnecessary re-renders
const handleSongSelect = useCallback((song: Song) => { const handleSongSelect = useCallback((song: Song) => {
setSelectedSong(song); setSelectedSong(song);
}, []); }, []);
// Format total duration for display
const getFormattedTotalDuration = useCallback((durationSeconds?: number): string => {
if (!durationSeconds) return "";
return formatTotalDuration(durationSeconds);
}, []);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const initialLoadDone = useRef(false); const initialLoadDone = useRef(false);
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const { isOpen: isWelcomeOpen, onClose: onWelcomeClose } = useDisclosure({ defaultIsOpen: true }); const { isOpen: isWelcomeOpen, onOpen: onWelcomeOpen, onClose: onWelcomeClose } = useDisclosure({ defaultIsOpen: false });
const isMobile = useBreakpointValue({ base: true, md: false }); const isMobile = useBreakpointValue({ base: true, md: false });
const [sidebarWidth, setSidebarWidth] = useState(400); const [sidebarWidth, setSidebarWidth] = useState(400);
@ -96,11 +107,38 @@ export default function RekordboxReader() {
loading: songsLoading, loading: songsLoading,
hasMore, hasMore,
totalSongs, totalSongs,
totalDuration,
loadNextPage, loadNextPage,
searchSongs, searchSongs,
searchQuery searchQuery
} = usePaginatedSongs({ pageSize: 50, playlistName: currentPlaylist }); } = usePaginatedSongs({ pageSize: 50, playlistName: currentPlaylist });
// Check if database is initialized (has songs or playlists) - moved after useDisclosure
useEffect(() => {
const checkDatabaseInitialized = async () => {
try {
// Check if there are any songs in the database
const songCount = await api.getSongCount();
const hasPlaylists = playlists.length > 0;
const isInitialized = songCount > 0 || hasPlaylists;
setIsDatabaseInitialized(isInitialized);
// Only show welcome modal if database is truly empty
if (!isInitialized) {
onWelcomeOpen();
}
} catch (error) {
// If we can't get the song count, assume database is not initialized
setIsDatabaseInitialized(false);
onWelcomeOpen();
}
};
if (!xmlLoading) {
checkDatabaseInitialized();
}
}, [xmlLoading, playlists.length, onWelcomeOpen]);
useEffect(() => { useEffect(() => {
// Only run this check after the initial data load // Only run this check after the initial data load
if (!xmlLoading && playlists.length > 0) { if (!xmlLoading && playlists.length > 0) {
@ -313,16 +351,11 @@ export default function RekordboxReader() {
}; };
}, [isResizing]); }, [isResizing]);
if (xmlLoading || songsLoading) { if (xmlLoading) {
return ( return (
<Flex height="100vh" align="center" justify="center" direction="column" gap={4}> <Flex height="100vh" align="center" justify="center" direction="column" gap={4}>
<Spinner size="xl" /> <Spinner size="xl" />
<Text>Loading your library...</Text> <Text>Loading your library...</Text>
{currentPlaylist !== "All Songs" && (
<Text fontSize="sm" color="gray.500">
Navigating to playlist: {currentPlaylist}
</Text>
)}
</Flex> </Flex>
); );
} }
@ -355,7 +388,7 @@ export default function RekordboxReader() {
userSelect={isResizing ? 'none' : 'auto'} userSelect={isResizing ? 'none' : 'auto'}
> >
{/* Welcome Modal */} {/* Welcome Modal */}
{!xmlLoading && !songsLoading && songs.length === 0 && ( {!xmlLoading && !isDatabaseInitialized && (
<Modal isOpen={isWelcomeOpen} onClose={onWelcomeClose} isCentered> <Modal isOpen={isWelcomeOpen} onClose={onWelcomeClose} isCentered>
<ModalOverlay /> <ModalOverlay />
<ModalContent bg="gray.800" maxW="md"> <ModalContent bg="gray.800" maxW="md">
@ -510,6 +543,7 @@ export default function RekordboxReader() {
loading={songsLoading} loading={songsLoading}
hasMore={hasMore} hasMore={hasMore}
totalSongs={totalSongs} totalSongs={totalSongs}
totalPlaylistDuration={getFormattedTotalDuration(totalDuration)}
onLoadMore={loadNextPage} onLoadMore={loadNextPage}
onSearch={searchSongs} onSearch={searchSongs}
searchQuery={searchQuery} searchQuery={searchQuery}

View File

@ -34,6 +34,7 @@ interface PaginatedSongListProps {
loading: boolean; loading: boolean;
hasMore: boolean; hasMore: boolean;
totalSongs: number; totalSongs: number;
totalPlaylistDuration?: string; // Total duration of the entire playlist
onLoadMore: () => void; onLoadMore: () => void;
onSearch: (query: string) => void; onSearch: (query: string) => void;
searchQuery: string; searchQuery: string;
@ -111,6 +112,7 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
loading, loading,
hasMore, hasMore,
totalSongs, totalSongs,
totalPlaylistDuration,
onLoadMore, onLoadMore,
onSearch, onSearch,
searchQuery, searchQuery,
@ -121,6 +123,20 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
const observerRef = useRef<IntersectionObserver | null>(null); const observerRef = useRef<IntersectionObserver | null>(null);
const loadingRef = useRef<HTMLDivElement>(null); const loadingRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null); const scrollContainerRef = useRef<HTMLDivElement>(null);
const isTriggeringRef = useRef(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
// Store current values in refs to avoid stale closures
const hasMoreRef = useRef(hasMore);
const loadingRef_state = useRef(loading);
const onLoadMoreRef = useRef(onLoadMore);
// Update refs when props change
useEffect(() => {
hasMoreRef.current = hasMore;
loadingRef_state.current = loading;
onLoadMoreRef.current = onLoadMore;
}, [hasMore, loading, onLoadMore]);
// Debounce search to prevent excessive API calls // Debounce search to prevent excessive API calls
const debouncedSearchQuery = useDebounce(localSearchQuery, 300); const debouncedSearchQuery = useDebounce(localSearchQuery, 300);
@ -199,15 +215,19 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
)); ));
}, [songs, selectedSongs, selectedSongId, handleSongSelect, toggleSelection, depth]); }, [songs, selectedSongs, selectedSongId, handleSongSelect, toggleSelection, depth]);
// Memoized total duration calculation // Use total playlist duration if available, otherwise calculate from current songs
const totalDuration = useMemo(() => { const totalDuration = useMemo(() => {
if (totalPlaylistDuration) {
return totalPlaylistDuration;
}
// Fallback to calculating from current songs
const totalSeconds = songs.reduce((total, song) => { const totalSeconds = songs.reduce((total, song) => {
if (!song.totalTime) return total; if (!song.totalTime) return total;
const seconds = Math.floor(Number(song.totalTime) / (song.totalTime.length > 4 ? 1000 : 1)); const seconds = Math.floor(Number(song.totalTime) / (song.totalTime.length > 4 ? 1000 : 1));
return total + seconds; return total + seconds;
}, 0); }, 0);
return formatTotalDuration(totalSeconds); return formatTotalDuration(totalSeconds);
}, [songs]); }, [songs, totalPlaylistDuration]);
// Memoized playlist options for bulk actions // Memoized playlist options for bulk actions
const playlistOptions = useMemo(() => { const playlistOptions = useMemo(() => {
@ -230,8 +250,14 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
if (loadingRef.current) { if (loadingRef.current) {
observerRef.current = new IntersectionObserver( observerRef.current = new IntersectionObserver(
(entries) => { (entries) => {
if (entries[0].isIntersecting && hasMore && !loading) { // Use current values from refs to avoid stale closure
onLoadMore(); if (entries[0].isIntersecting && hasMoreRef.current && !loadingRef_state.current && !isTriggeringRef.current) {
isTriggeringRef.current = true;
onLoadMoreRef.current();
// Reset the flag after a short delay to prevent multiple triggers
timeoutRef.current = setTimeout(() => {
isTriggeringRef.current = false;
}, 1000);
} }
}, },
{ {
@ -247,8 +273,11 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
if (observerRef.current) { if (observerRef.current) {
observerRef.current.disconnect(); observerRef.current.disconnect();
} }
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
}; };
}, [hasMore, loading, onLoadMore]); }, []); // Remove dependencies to prevent recreation
return ( return (
<Flex direction="column" height="100%"> <Flex direction="column" height="100%">
@ -298,6 +327,11 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
</Checkbox> </Checkbox>
<Text color="gray.400" fontSize="sm"> <Text color="gray.400" fontSize="sm">
{songs.length} of {totalSongs} songs {totalDuration} {songs.length} of {totalSongs} songs {totalDuration}
{hasMore && songs.length > 0 && (
<Text as="span" color="blue.400" ml={2}>
Scroll for more
</Text>
)}
</Text> </Text>
</HStack> </HStack>
@ -354,14 +388,26 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
{/* Loading indicator for infinite scroll */} {/* Loading indicator for infinite scroll */}
{loading && ( {loading && (
<Flex justify="center" p={4} key="loading-spinner"> <Flex justify="center" align="center" p={6} key="loading-spinner" gap={3}>
<Spinner size="md" color="blue.400" /> <Spinner size="md" color="blue.400" />
<Text color="gray.400" fontSize="sm">
Loading more songs...
</Text>
</Flex> </Flex>
)} )}
{/* Intersection observer target */} {/* Intersection observer target */}
<div ref={loadingRef} style={{ height: '20px' }} key="intersection-target" /> <div ref={loadingRef} style={{ height: '20px' }} key="intersection-target" />
{/* Loading more indicator (subtle) */}
{!loading && hasMore && songs.length > 0 && (
<Flex justify="center" p={3} key="loading-more-indicator">
<Text color="gray.600" fontSize="xs">
Scroll for more songs
</Text>
</Flex>
)}
{/* End of results message */} {/* End of results message */}
{!hasMore && songs.length > 0 && ( {!hasMore && songs.length > 0 && (
<Flex justify="center" p={4} key="end-message"> <Flex justify="center" p={4} key="end-message">

View File

@ -78,7 +78,7 @@ const PlaylistItem: React.FC<PlaylistItemProps> = ({
allFolders, allFolders,
}) => { }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const indent = level * 10; const indent = level * 10; // Reverted back to 10px per level
if (node.type === 'folder') { if (node.type === 'folder') {
return ( return (
@ -89,7 +89,9 @@ const PlaylistItem: React.FC<PlaylistItemProps> = ({
{...getButtonStyles(false)} {...getButtonStyles(false)}
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
ml={indent} ml={indent}
pl={level > 0 ? 6 : 4} // Add extra padding for nested items
justifyContent="flex-start" justifyContent="flex-start"
position="relative"
leftIcon={ leftIcon={
<Box position="relative" display="flex" alignItems="center"> <Box position="relative" display="flex" alignItems="center">
<ChevronRightIcon <ChevronRightIcon
@ -102,17 +104,29 @@ const PlaylistItem: React.FC<PlaylistItemProps> = ({
</Box> </Box>
} }
> >
<Icon {level > 0 && (
viewBox="0 0 24 24" <Box
color="blue.300" position="absolute"
ml={1} left="8px"
mr={2} top="50%"
> transform="translateY(-50%)"
<path width="2px"
fill="currentColor" height="12px"
d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10z" bg="gray.500"
/> borderRadius="1px"
</Icon> />
)}
<Icon
viewBox="0 0 24 24"
color="blue.300"
ml={1}
mr={2}
>
<path
fill="currentColor"
d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10z"
/>
</Icon>
{node.name} {node.name}
</Button> </Button>
</Flex> </Flex>
@ -143,10 +157,24 @@ const PlaylistItem: React.FC<PlaylistItemProps> = ({
{...getButtonStyles(selectedItem === node.name)} {...getButtonStyles(selectedItem === node.name)}
onClick={() => onPlaylistSelect(node.name)} onClick={() => onPlaylistSelect(node.name)}
ml={indent} ml={indent}
pl={level > 0 ? 6 : 4} // Add extra padding for nested items
borderRightRadius={0} borderRightRadius={0}
borderRight="1px solid" borderRight="1px solid"
borderRightColor="whiteAlpha.200" borderRightColor="whiteAlpha.200"
position="relative"
> >
{level > 0 && (
<Box
position="absolute"
left="8px"
top="50%"
transform="translateY(-50%)"
width="2px"
height="12px"
bg="gray.500"
borderRadius="1px"
/>
)}
{node.name} {node.name}
</Button> </Button>
<Menu> <Menu>
@ -266,6 +294,39 @@ export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
> >
All Songs All Songs
</Button> </Button>
{/* Add Playlist and Folder buttons at the top */}
<Flex gap={2} mb={2}>
<Button
onClick={onPlaylistModalOpen}
colorScheme="blue"
size="sm"
flex={1}
leftIcon={<AddIcon />}
borderRadius="md"
_hover={{
transform: "translateY(-1px)",
boxShadow: "sm",
}}
>
New Playlist
</Button>
<Button
onClick={onFolderModalOpen}
colorScheme="teal"
size="sm"
flex={1}
leftIcon={<ViewIcon />}
borderRadius="md"
_hover={{
transform: "translateY(-1px)",
boxShadow: "sm",
}}
>
New Folder
</Button>
</Flex>
{playlists.map((node, index) => ( {playlists.map((node, index) => (
<PlaylistItem <PlaylistItem
key={node.id || index} key={node.id || index}
@ -280,37 +341,6 @@ export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
))} ))}
</VStack> </VStack>
<Flex gap={2}>
<Button
onClick={onPlaylistModalOpen}
colorScheme="blue"
size="sm"
flex={1}
leftIcon={<AddIcon />}
borderRadius="md"
_hover={{
transform: "translateY(-1px)",
boxShadow: "sm",
}}
>
New Playlist
</Button>
<Button
onClick={onFolderModalOpen}
colorScheme="teal"
size="sm"
flex={1}
leftIcon={<ViewIcon />}
borderRadius="md"
_hover={{
transform: "translateY(-1px)",
boxShadow: "sm",
}}
>
New Folder
</Button>
</Flex>
{/* New Playlist Modal */} {/* New Playlist Modal */}
<Modal <Modal
isOpen={isPlaylistModalOpen} isOpen={isPlaylistModalOpen}

View File

@ -18,10 +18,13 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [searchQuery, setSearchQuery] = useState(initialSearch); const [searchQuery, setSearchQuery] = useState(initialSearch);
const [totalSongs, setTotalSongs] = useState(0); const [totalSongs, setTotalSongs] = useState(0);
const [totalDuration, setTotalDuration] = useState<number | undefined>(undefined);
const [isInitialLoad, setIsInitialLoad] = useState(true); const [isInitialLoad, setIsInitialLoad] = useState(true);
const loadingRef = useRef(false); const loadingRef = useRef(false);
const currentPlaylistRef = useRef(playlistName); const currentPlaylistRef = useRef(playlistName);
const currentSearchQueryRef = useRef(searchQuery);
const previousPlaylistRef = useRef(playlistName);
const abortControllerRef = useRef<AbortController | null>(null); const abortControllerRef = useRef<AbortController | null>(null);
// Cleanup function to prevent memory leaks // Cleanup function to prevent memory leaks
@ -34,10 +37,11 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
}, []); }, []);
// Load songs for a specific page // Load songs for a specific page
const loadPage = useCallback(async (page: number, search: string = searchQuery, targetPlaylist?: string) => { const loadPage = useCallback(async (page: number, search?: string, targetPlaylist?: string) => {
if (loadingRef.current) return; if (loadingRef.current) return;
const playlistToUse = targetPlaylist || playlistName; const searchToUse = search ?? currentSearchQueryRef.current;
const playlistToUse = targetPlaylist ?? currentPlaylistRef.current;
// Cleanup previous request // Cleanup previous request
cleanup(); cleanup();
@ -54,10 +58,10 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
if (playlistToUse && playlistToUse !== 'All Songs') { if (playlistToUse && playlistToUse !== 'All Songs') {
// Load songs for specific playlist // Load songs for specific playlist
response = await api.getPlaylistSongsPaginated(playlistToUse, page, pageSize, search); response = await api.getPlaylistSongsPaginated(playlistToUse, page, pageSize, searchToUse);
} else { } else {
// Load all songs // Load all songs
response = await api.getSongsPaginated(page, pageSize, search); response = await api.getSongsPaginated(page, pageSize, searchToUse);
} }
// Check if request was aborted // Check if request was aborted
@ -75,6 +79,7 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
setHasMore(response.pagination.hasNextPage); setHasMore(response.pagination.hasNextPage);
setTotalSongs(response.pagination.totalSongs); setTotalSongs(response.pagination.totalSongs);
setTotalDuration(response.totalDuration);
setCurrentPage(page); setCurrentPage(page);
} catch (err) { } catch (err) {
// Don't set error if request was aborted // Don't set error if request was aborted
@ -88,7 +93,7 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
setIsInitialLoad(false); setIsInitialLoad(false);
} }
} }
}, [pageSize, searchQuery, playlistName, cleanup]); }, [pageSize, cleanup]); // Remove searchQuery and playlistName from dependencies
// Load next page (for infinite scroll) // Load next page (for infinite scroll)
const loadNextPage = useCallback(() => { const loadNextPage = useCallback(() => {
@ -100,7 +105,8 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
// Search songs with debouncing // Search songs with debouncing
const searchSongs = useCallback((query: string) => { const searchSongs = useCallback((query: string) => {
setSearchQuery(query); setSearchQuery(query);
// Don't clear songs immediately - let the new search results replace them // Clear songs for new search to replace them
setSongs([]);
setHasMore(true); setHasMore(true);
setCurrentPage(1); setCurrentPage(1);
setError(null); setError(null);
@ -131,19 +137,22 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
// Handle playlist changes // Handle playlist changes
useEffect(() => { useEffect(() => {
if (currentPlaylistRef.current !== playlistName) { if (!isInitialLoad && previousPlaylistRef.current !== playlistName) {
// Update refs
currentPlaylistRef.current = playlistName; currentPlaylistRef.current = playlistName;
if (!isInitialLoad) { currentSearchQueryRef.current = searchQuery;
// Don't clear songs immediately - let the new playlist results replace them previousPlaylistRef.current = playlistName;
setHasMore(true);
setCurrentPage(1); // Clear songs for new playlist to replace them
setSearchQuery(initialSearch); setSongs([]);
setError(null); setHasMore(true);
// Use setTimeout to avoid the dependency issue setCurrentPage(1);
setTimeout(() => { setSearchQuery(initialSearch);
loadPage(1, initialSearch, playlistName); setError(null);
}, 0); // Use setTimeout to avoid the dependency issue
} setTimeout(() => {
loadPage(1, initialSearch, playlistName);
}, 0);
} }
}, [playlistName, isInitialLoad, initialSearch, loadPage]); }, [playlistName, isInitialLoad, initialSearch, loadPage]);
@ -153,6 +162,7 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
error, error,
hasMore, hasMore,
totalSongs, totalSongs,
totalDuration,
currentPage, currentPage,
searchQuery, searchQuery,
isInitialLoad, isInitialLoad,

View File

@ -14,6 +14,7 @@ export interface PaginationInfo {
export interface SongsResponse { export interface SongsResponse {
songs: Song[]; songs: Song[];
pagination: PaginationInfo; pagination: PaginationInfo;
totalDuration?: number; // Total duration of the entire playlist in seconds
} }
class Api { class Api {