Files
lidify/frontend/features/library/hooks/useLibraryData.ts
Daniel Thiberge 567f38e1ea fix: remove 500/1000 item cap on library pagination (Issue #12)
- Remove Math.min(..., 1000) hard cap on /library/artists endpoint
- Remove Math.min(..., 1000) hard cap on /library/albums endpoint
- Add offset parameter and total count to /library/tracks endpoint
- Rewrite useLibraryData hook for true server-side pagination
- Update library page to fetch pages on demand instead of client-side slicing
- Disable pagination buttons during loading to prevent race conditions

This allows libraries of any size to be fully browsable. Previously,
users with 10,000+ songs were capped at seeing only 500 items.

Fixes #12

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 02:44:12 -05:00

164 lines
5.7 KiB
TypeScript

import { useEffect, useState, useCallback, useRef } from "react";
import { Artist, Album, Track, Tab } from "../types";
import { api } from "@/lib/api";
import { useAuth } from "@/lib/auth-context";
export type LibraryFilter = "owned" | "discovery" | "all";
export type SortOption = "name" | "name-desc" | "recent" | "tracks";
interface UseLibraryDataProps {
activeTab: Tab;
filter?: LibraryFilter;
sortBy?: SortOption;
itemsPerPage?: number;
currentPage?: number;
}
interface PaginationState {
total: number;
offset: number;
limit: number;
}
export function useLibraryData({
activeTab,
filter = "owned",
sortBy = "name",
itemsPerPage = 50,
currentPage = 1,
}: UseLibraryDataProps) {
const [artists, setArtists] = useState<Artist[]>([]);
const [albums, setAlbums] = useState<Album[]>([]);
const [tracks, setTracks] = useState<Track[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [pagination, setPagination] = useState<PaginationState>({
total: 0,
offset: 0,
limit: itemsPerPage,
});
const { isAuthenticated } = useAuth();
// Track the current request to avoid race conditions
const requestIdRef = useRef(0);
const loadData = useCallback(async () => {
if (!isAuthenticated) return;
const currentRequestId = ++requestIdRef.current;
const offset = (currentPage - 1) * itemsPerPage;
setIsLoading(true);
try {
if (activeTab === "artists") {
const response = await api.getArtists({
limit: itemsPerPage,
offset,
filter,
});
// Only update if this is still the current request
if (currentRequestId === requestIdRef.current) {
// Sort client-side since backend sorts by name asc only
let sortedArtists = [...response.artists];
switch (sortBy) {
case "name":
sortedArtists.sort((a, b) => a.name.localeCompare(b.name));
break;
case "name-desc":
sortedArtists.sort((a, b) => b.name.localeCompare(a.name));
break;
case "tracks":
sortedArtists.sort((a, b) => (b.trackCount || 0) - (a.trackCount || 0));
break;
}
setArtists(sortedArtists);
setPagination({
total: response.total,
offset: response.offset,
limit: response.limit,
});
}
} else if (activeTab === "albums") {
const response = await api.getAlbums({
limit: itemsPerPage,
offset,
filter,
});
if (currentRequestId === requestIdRef.current) {
// Sort client-side since backend sorts by year desc only
let sortedAlbums = [...response.albums];
switch (sortBy) {
case "name":
sortedAlbums.sort((a, b) => a.title.localeCompare(b.title));
break;
case "name-desc":
sortedAlbums.sort((a, b) => b.title.localeCompare(a.title));
break;
case "recent":
sortedAlbums.sort((a, b) => (b.year || 0) - (a.year || 0));
break;
}
setAlbums(sortedAlbums);
setPagination({
total: response.total,
offset: response.offset,
limit: response.limit,
});
}
} else if (activeTab === "tracks") {
const response = await api.getTracks({
limit: itemsPerPage,
offset,
});
if (currentRequestId === requestIdRef.current) {
// Sort client-side
let sortedTracks = [...response.tracks];
switch (sortBy) {
case "name":
sortedTracks.sort((a, b) => a.title.localeCompare(b.title));
break;
case "name-desc":
sortedTracks.sort((a, b) => b.title.localeCompare(a.title));
break;
}
setTracks(sortedTracks);
setPagination({
total: response.total,
offset: response.offset,
limit: response.limit,
});
}
}
} catch (error) {
console.error("Failed to load library data:", error);
} finally {
if (currentRequestId === requestIdRef.current) {
setIsLoading(false);
}
}
}, [activeTab, filter, sortBy, itemsPerPage, currentPage, isAuthenticated]);
useEffect(() => {
loadData();
}, [loadData]);
const reloadData = () => {
loadData();
};
const totalPages = Math.ceil(pagination.total / itemsPerPage);
return {
artists,
albums,
tracks,
isLoading,
reloadData,
pagination: {
...pagination,
totalPages,
currentPage,
itemsPerPage,
},
};
}