import React, { useEffect, useState, useCallback, useRef } from 'react'; import AlbumCard from '../components/AlbumCard'; import GenreFilterBar from '../components/GenreFilterBar'; import { getAlbumList, getAlbumsByGenre, SubsonicAlbum } from '../api/subsonic'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '../store/authStore'; import { X } from 'lucide-react'; type SortType = 'alphabeticalByName' | 'alphabeticalByArtist'; const PAGE_SIZE = 30; const CURRENT_YEAR = new Date().getFullYear(); async function fetchByGenres(genres: string[]): Promise { const results = await Promise.all(genres.map(g => getAlbumsByGenre(g, 500, 0))); const seen = new Set(); return results.flat().filter(a => { if (seen.has(a.id)) return false; seen.add(a.id); return true; }); } export default function Albums() { const { t } = useTranslation(); const musicLibraryFilterVersion = useAuthStore(s => s.musicLibraryFilterVersion); const [albums, setAlbums] = useState([]); const [sort, setSort] = useState('alphabeticalByName'); const [loading, setLoading] = useState(true); const [page, setPage] = useState(0); const [hasMore, setHasMore] = useState(true); const [selectedGenres, setSelectedGenres] = useState([]); const [yearFrom, setYearFrom] = useState(''); const [yearTo, setYearTo] = useState(''); const observerTarget = useRef(null); const genreFiltered = selectedGenres.length > 0; const fromNum = parseInt(yearFrom, 10); const toNum = parseInt(yearTo, 10); const yearActive = !isNaN(fromNum) && !isNaN(toNum) && fromNum >= 1 && toNum >= 1; const load = useCallback(async ( sortType: SortType, offset: number, append = false, yearFilter?: { from: number; to: number }, ) => { setLoading(true); try { const extra = yearFilter ? { fromYear: yearFilter.from, toYear: yearFilter.to } : {}; const type = yearFilter ? 'byYear' : sortType; const data = await getAlbumList(type, PAGE_SIZE, offset, extra); if (append) setAlbums(prev => [...prev, ...data]); else setAlbums(data); setHasMore(data.length === PAGE_SIZE); } finally { setLoading(false); } }, [musicLibraryFilterVersion]); const loadFiltered = useCallback(async (genres: string[], sortType: SortType) => { setLoading(true); try { const data = await fetchByGenres(genres); const sorted = [...data].sort((a, b) => sortType === 'alphabeticalByArtist' ? a.artist.localeCompare(b.artist) : a.name.localeCompare(b.name) ); setAlbums(sorted); setHasMore(false); } finally { setLoading(false); } }, [musicLibraryFilterVersion]); useEffect(() => { setPage(0); if (genreFiltered) { loadFiltered(selectedGenres, sort); } else if (yearActive) { load(sort, 0, false, { from: fromNum, to: toNum }); } else { load(sort, 0); } }, [sort, genreFiltered, selectedGenres, yearActive, fromNum, toNum, load, loadFiltered]); const loadMore = useCallback(() => { if (loading || !hasMore || genreFiltered) return; const next = page + 1; setPage(next); const yf = yearActive ? { from: fromNum, to: toNum } : undefined; load(sort, next * PAGE_SIZE, true, yf); }, [loading, hasMore, page, sort, load, genreFiltered, yearActive, fromNum, toNum]); useEffect(() => { const observer = new IntersectionObserver( entries => { if (entries[0].isIntersecting) loadMore(); }, { rootMargin: '200px' } ); if (observerTarget.current) observer.observe(observerTarget.current); return () => observer.disconnect(); }, [loadMore]); const clearYear = () => { setYearFrom(''); setYearTo(''); }; const sortOptions: { value: SortType; label: string }[] = [ { value: 'alphabeticalByName', label: t('albums.sortByName') }, { value: 'alphabeticalByArtist', label: t('albums.sortByArtist') }, ]; return (

{t('albums.title')}

{!yearActive && sortOptions.map(o => ( ))} {/* Year range filter */}
{t('albums.yearFilterLabel')} setYearFrom(e.target.value)} style={{ width: 68, padding: '4px 6px', fontSize: 12 }} /> setYearTo(e.target.value)} style={{ width: 68, padding: '4px 6px', fontSize: 12 }} /> {yearActive && ( )}
{loading && albums.length === 0 ? (
) : ( <>
{albums.map(a => )}
{!genreFiltered && (
{loading && hasMore &&
}
)} )}
); }