import React, { useEffect, useState, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { getArtists, SubsonicArtist } from '../api/subsonic'; import { LayoutGrid, List } from 'lucide-react'; import { usePlayerStore } from '../store/playerStore'; import { useTranslation } from 'react-i18next'; const ALL_SENTINEL = 'ALL'; const ALPHABET = [ALL_SENTINEL, '#', ...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')]; // 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)', ]; 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]; } function nameInitial(name: string): string { // Skip leading non-letter chars (punctuation, numbers, brackets, …) const letter = name.match(/[a-zA-ZÀ-ÖØ-öø-ÿ]/)?.[0]; if (letter) return letter.toUpperCase(); // Fallback: first alphanumeric (e.g. "1349") const alnum = name.match(/[a-zA-Z0-9]/)?.[0]; return alnum?.toUpperCase() ?? '?'; } export default function Artists() { const { t } = useTranslation(); const [artists, setArtists] = useState([]); const [loading, setLoading] = useState(true); const [filter, setFilter] = useState(''); const [letterFilter, setLetterFilter] = useState(ALL_SENTINEL); const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); const [visibleCount, setVisibleCount] = useState(50); const navigate = useNavigate(); const openContextMenu = usePlayerStore(state => state.openContextMenu); useEffect(() => { getArtists().then(data => { setArtists(data); setLoading(false); }).catch(() => setLoading(false)); }, []); const loadMore = useCallback(() => { setVisibleCount(prev => prev + 50); }, []); // Reset infinite scroll when filters change useEffect(() => { setVisibleCount(50); }, [filter, letterFilter, viewMode]); // Filter pipeline let filtered = artists; if (letterFilter !== ALL_SENTINEL) { filtered = filtered.filter(a => { const first = a.name[0]?.toUpperCase() ?? '#'; const isAlpha = /^[A-Z]$/.test(first); if (letterFilter === '#') return !isAlpha; return first === letterFilter; }); } if (filter) { filtered = filtered.filter(a => a.name.toLowerCase().includes(filter.toLowerCase())); } const visible = filtered.slice(0, visibleCount); const hasMore = visibleCount < filtered.length; // Group by first letter (for list view) const groups: Record = {}; visible.forEach(a => { const letter = a.name[0]?.toUpperCase() ?? '#'; const key = /^[A-Z]$/.test(letter) ? letter : '#'; if (!groups[key]) groups[key] = []; groups[key].push(a); }); const letters = Object.keys(groups).sort(); return (

{t('artists.title')}

setFilter(e.target.value)} id="artist-filter-input" />
{ALPHABET.map(l => ( ))}
{loading &&
} {!loading && viewMode === 'grid' && (
{visible.map(artist => { const color = nameColor(artist.name); return (
navigate(`/artist/${artist.id}`)} onContextMenu={(e) => { e.preventDefault(); openContextMenu(e.clientX, e.clientY, artist, 'artist'); }} >
{nameInitial(artist.name)}
{artist.name}
{artist.albumCount != null && (
{t('artists.albumCount', { count: artist.albumCount })}
)}
); })}
)} {!loading && viewMode === 'list' && ( <> {letters.map(letter => (

{letter}

{groups[letter].map(artist => { const color = nameColor(artist.name); return ( ); })}
))} )} {!loading && hasMore && (
)} {!loading && filtered.length === 0 && (
{t('artists.notFound')}
)}
); }