import React, { useCallback, useMemo, useState } from 'react'; import { Play, HardDriveDownload, Trash2, ListPlus } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useOfflineStore, type OfflineAlbumMeta } from '../store/offlineStore'; import { useAuthStore } from '../store/authStore'; import { usePlayerStore } from '../store/playerStore'; import { CoverArtImage } from '../cover/CoverArtImage'; import { usePerfProbeFlags } from '../utils/perf/perfFlags'; import { albumGridWarmCovers } from '../cover/layoutSizes'; import { VirtualCardGrid } from '../components/VirtualCardGrid'; import { buildOfflineTracksForAlbum, ensureServerForOfflineAlbum, offlineAlbumCoverScope, offlineTrackCount, } from '../utils/offline/offlineLibraryHelpers'; const OFFLINE_CARD_COVER_CSS_PX = 300; import { showToast } from '../utils/ui/toast'; type FilterType = 'all' | 'album' | 'playlist' | 'artist'; export default function OfflineLibrary() { const { t } = useTranslation(); const perfFlags = usePerfProbeFlags(); const servers = useAuthStore(s => s.servers); const serverNames = useMemo( () => Object.fromEntries(servers.map(s => [s.id, s.name])), [servers], ); const showServerLabels = servers.length > 1; const offlineAlbums = useOfflineStore(s => s.albums); const offlineTracks = useOfflineStore(s => s.tracks); const deleteAlbum = useOfflineStore(s => s.deleteAlbum); const playTrack = usePlayerStore(s => s.playTrack); const enqueue = usePlayerStore(s => s.enqueue); const [filter, setFilter] = useState('all'); const albums = useMemo( () => Object.values(offlineAlbums).sort((a, b) => a.name.localeCompare(b.name)), [offlineAlbums], ); const countByType = (type: FilterType) => { if (type === 'all') return albums.length; return albums.filter(a => (a.type ?? 'album') === type).length; }; const filtered = filter === 'all' ? albums : albums.filter(a => (a.type ?? 'album') === filter); const runWithAlbumServer = useCallback(async ( album: OfflineAlbumMeta, action: () => void, ) => { const ok = await ensureServerForOfflineAlbum(album); if (!ok) { showToast(t('connection.switchFailed'), 4500, 'error'); return; } action(); }, [t]); const handlePlay = (album: OfflineAlbumMeta) => { void runWithAlbumServer(album, () => { const tracks = buildOfflineTracksForAlbum(album, offlineTracks); if (tracks[0]) playTrack(tracks[0], tracks); }); }; const handleEnqueue = (album: OfflineAlbumMeta) => { void runWithAlbumServer(album, () => { enqueue(buildOfflineTracksForAlbum(album, offlineTracks)); }); }; const renderCard = (album: OfflineAlbumMeta) => { const coverScope = offlineAlbumCoverScope(album); const trackCount = offlineTrackCount(album, offlineTracks); const serverLabel = serverNames[album.serverId]; return (
{coverScope && album.coverArt ? ( ) : (
)}

{album.name}

{album.artist}

{showServerLabels && serverLabel && (

{t('connection.offlineCachedOnServer', { server: serverLabel })}

)} {album.year &&

{album.year}

}
{t('albumDetail.tracksCount', { n: trackCount })}
); }; const renderArtistGroups = () => { const groups: Record = {}; for (const album of filtered) { const key = album.artist || '—'; if (!groups[key]) groups[key] = []; groups[key].push(album); } const sortedArtists = Object.keys(groups).sort((a, b) => a.localeCompare(b)); return sortedArtists.map(artistName => (

{artistName}

`${a.serverId}:${a.id}`} rowVariant="album" disableVirtualization={perfFlags.disableMainstageVirtualLists} layoutSignal={groups[artistName].length} warmGridCovers={albumGridWarmCovers(OFFLINE_CARD_COVER_CSS_PX)} renderItem={renderCard} />
)); }; const TABS: { id: FilterType; labelKey: string }[] = [ { id: 'all', labelKey: 'connection.offlineFilterAll' }, { id: 'album', labelKey: 'connection.offlineFilterAlbums' }, { id: 'playlist', labelKey: 'connection.offlineFilterPlaylists' }, { id: 'artist', labelKey: 'connection.offlineFilterArtists' }, ]; return (

{t('connection.offlineLibraryTitle')}

{t('connection.offlineAlbumCount', { n: albums.length, count: albums.length })}

{TABS.map(tab => { const count = countByType(tab.id); if (tab.id !== 'all' && count === 0) return null; return ( ); })}
{filtered.length === 0 ? (
{t('connection.offlineLibraryEmpty')}
) : filter === 'artist' ? ( renderArtistGroups() ) : ( `${a.serverId}:${a.id}`} rowVariant="album" disableVirtualization={perfFlags.disableMainstageVirtualLists} layoutSignal={filtered.length} warmGridCovers={albumGridWarmCovers(OFFLINE_CARD_COVER_CSS_PX)} renderItem={renderCard} /> )}
); }