mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
refactor(artists): I.4 — split Artists.tsx 520 → 233 LOC across 6 files (#676)
* refactor(artists): extract helpers + constants Pull ALL_SENTINEL / ALPHABET / ARTIST_LIST_* row-height estimates, the ArtistListFlatRow union, CTP_COLORS palette, and the deterministic nameColor / nameInitial helpers into utils/artistsHelpers.ts. Artists.tsx: 520 → 496 LOC. * refactor(artists): extract ArtistAvatars subcomponents Pull ArtistCardAvatar (300px, grid view) and ArtistRowAvatar (64px, list view) into components/artists/ArtistAvatars.tsx. Both fall back to a hashed-Catppuccin monogram when artist images are off or no cover art is available. Artists.tsx: 496 → 436 LOC. * refactor(artists): extract useArtistsFiltering hook Bundle the letter/text/star filter pipeline, visible-slice memo, group-by-letter, and the virtualizer flat-rows list into hooks/useArtistsFiltering.ts. List-view-only outputs short-circuit when grid view is active. Artists.tsx: 436 → 386 LOC. * refactor(artists): extract useArtistsInfiniteScroll hook Bundle visibleCount + loadingMore state, the sentinel IntersectionObserver, loadMore callback, and the filter-change reset into hooks/useArtistsInfiniteScroll.ts. The observer no longer takes hasMore — the sentinel element only mounts while there is more data, so the observer attaches/detaches naturally with it. Artists.tsx: 386 → 370 LOC. * refactor(artists): extract ArtistsGridView + ArtistsListView Move the grid card layout to components/artists/ArtistsGridView.tsx and the dual-path list layout (non-virtualized fallback + virtualized stream) to components/artists/ArtistsListView.tsx. Both paths now share an internal ArtistListRow component so click + context-menu behaviour is identical regardless of which renderer is active. Artists.tsx: 370 → 233 LOC.
This commit is contained in:
committed by
GitHub
parent
25289bbf31
commit
6552d2a5cb
@@ -0,0 +1,61 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface UseArtistsInfiniteScrollArgs {
|
||||
pageSize: number;
|
||||
resetDeps: ReadonlyArray<unknown>;
|
||||
}
|
||||
|
||||
interface UseArtistsInfiniteScrollResult {
|
||||
visibleCount: number;
|
||||
loadingMore: boolean;
|
||||
observerTarget: React.RefObject<HTMLDivElement | null>;
|
||||
loadMore: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page through the artists list with a sentinel-driven
|
||||
* IntersectionObserver. `pageSize` is dynamic because artist-images
|
||||
* mode wants smaller batches to keep disk I/O sane on big libraries
|
||||
* (5000+ artists).
|
||||
*
|
||||
* `resetDeps` is the list of values that should snap `visibleCount`
|
||||
* back to one page — filter text, letter pick, starred-only,
|
||||
* view-mode, page-size itself.
|
||||
*
|
||||
* The observer doesn't take a `hasMore` flag — the page only renders
|
||||
* the sentinel `<div ref={observerTarget}>` while there is more data,
|
||||
* so the observer naturally disconnects when the last page is reached
|
||||
* (the cleanup runs as the sentinel unmounts).
|
||||
*/
|
||||
export function useArtistsInfiniteScroll({
|
||||
pageSize,
|
||||
resetDeps,
|
||||
}: UseArtistsInfiniteScrollArgs): UseArtistsInfiniteScrollResult {
|
||||
const [visibleCount, setVisibleCount] = useState(pageSize);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const observerTarget = useRef<HTMLDivElement>(null);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (loadingMore) return;
|
||||
setLoadingMore(true);
|
||||
setVisibleCount(prev => prev + pageSize);
|
||||
setTimeout(() => setLoadingMore(false), 100);
|
||||
}, [loadingMore, pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
setVisibleCount(pageSize);
|
||||
// resetDeps is intentionally spread into the dep array.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pageSize, ...resetDeps]);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
entries => { if (entries[0].isIntersecting) loadMore(); },
|
||||
{ rootMargin: '200px' },
|
||||
);
|
||||
if (observerTarget.current) observer.observe(observerTarget.current);
|
||||
return () => observer.disconnect();
|
||||
}, [loadMore]);
|
||||
|
||||
return { visibleCount, loadingMore, observerTarget, loadMore };
|
||||
}
|
||||
Reference in New Issue
Block a user