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:
Frank Stellmacher
2026-05-14 00:33:27 +02:00
committed by GitHub
parent 25289bbf31
commit 6552d2a5cb
7 changed files with 584 additions and 337 deletions
+35
View File
@@ -0,0 +1,35 @@
import type { SubsonicArtist } from '../api/subsonicTypes';
export const ALL_SENTINEL = 'ALL';
export const ALPHABET = [ALL_SENTINEL, '#', ...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')];
/** Virtual row height guesses — letter heading vs dense rows vs last row in section (group gap). */
export const ARTIST_LIST_LETTER_ROW_EST = 48;
export const ARTIST_LIST_ROW_EST = 64;
export const ARTIST_LIST_LAST_IN_LETTER_EST = 88;
export type ArtistListFlatRow =
| { kind: 'letter'; letter: string }
| { kind: 'artist'; artist: SubsonicArtist; isLastInLetter: boolean };
// Catppuccin accent colors — one is picked deterministically from the artist name
const CTP_COLORS = [
'var(--ctp-rosewater)', 'var(--ctp-flamingo)', 'var(--ctp-pink)', 'var(--ctp-mauve)',
'var(--ctp-red)', 'var(--ctp-maroon)', 'var(--ctp-peach)', 'var(--ctp-yellow)',
'var(--ctp-green)', 'var(--ctp-teal)', 'var(--ctp-sky)', 'var(--ctp-sapphire)',
'var(--ctp-blue)', 'var(--ctp-lavender)',
];
export function nameColor(name: string): string {
let h = 0;
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
return CTP_COLORS[h % CTP_COLORS.length];
}
export function nameInitial(name: string): string {
// \p{L} matches any Unicode letter — covers cyrillic, arabic, CJK, etc.
const letter = name.match(/\p{L}/u)?.[0];
if (letter) return letter.toUpperCase();
const alnum = name.match(/[0-9]/)?.[0];
return alnum ?? '?';
}