import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl'; import { getRandomAlbums, getAlbum } from '../api/subsonicLibrary'; import type { SubsonicAlbum } from '../api/subsonicTypes'; import { songToTrack } from '../utils/playback/songToTrack'; import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { Play, ListPlus } from 'lucide-react'; import CachedImage, { useCachedUrl } from './CachedImage'; import { usePlayerStore } from '../store/playerStore'; import { useTranslation } from 'react-i18next'; import { playAlbum } from '../utils/playback/playAlbum'; import { useIsMobile } from '../hooks/useIsMobile'; import { useWindowVisibility } from '../hooks/useWindowVisibility'; import { useAuthStore } from '../store/authStore'; import { useThemeStore } from '../store/themeStore'; import { filterAlbumsByMixRatings, getMixMinRatingsConfigFromAuth } from '../utils/mix/mixRatingFilter'; import { usePerfProbeFlags } from '../utils/perf/perfFlags'; const INTERVAL_MS = 10000; const HERO_ALBUM_COUNT = 8; /** Larger pool when mix rating filter is on so we can still fill the hero strip. */ const HERO_RANDOM_POOL = 32; // Crossfading background — same layer pattern as FullscreenPlayer function HeroBg({ url }: { url: string }) { const [layers, setLayers] = useState>(() => url ? [{ url, id: 0, visible: true }] : [] ); const counter = useRef(1); useEffect(() => { if (!url) return; const id = counter.current++; setLayers(prev => [...prev, { url, id, visible: false }]); const t1 = setTimeout(() => setLayers(prev => prev.map(l => ({ ...l, visible: l.id === id }))), 20); const t2 = setTimeout(() => setLayers(prev => prev.filter(l => l.id === id)), 900); return () => { clearTimeout(t1); clearTimeout(t2); }; }, [url]); return ( <> {layers.map(layer => (