mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
6552d2a5cb
* 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.
36 lines
1.4 KiB
TypeScript
36 lines
1.4 KiB
TypeScript
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 ?? '?';
|
|
}
|