import { Ionicons } from "@expo/vector-icons"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; import { useAtom } from "jotai"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { ActivityIndicator, TouchableOpacity, View } from "react-native"; import { Text } from "@/components/common/Text"; import { AnimatedEqualizer } from "@/components/music/AnimatedEqualizer"; import { useHaptic } from "@/hooks/useHaptic"; import { useNetworkStatus } from "@/hooks/useNetworkStatus"; import { audioStorageEvents, getLocalPath, isCached, isPermanentDownloading, isPermanentlyDownloaded, } from "@/providers/AudioStorage"; import { apiAtom } from "@/providers/JellyfinProvider"; import { useMusicPlayer } from "@/providers/MusicPlayerProvider"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { formatDuration } from "@/utils/time"; interface Props { track: BaseItemDto; index?: number; queue?: BaseItemDto[]; showArtwork?: boolean; onOptionsPress?: (track: BaseItemDto) => void; } export const MusicTrackItem: React.FC = ({ track, index: _index, queue, showArtwork = true, onOptionsPress, }) => { const [api] = useAtom(apiAtom); const { playTrack, currentTrack, isPlaying, loadingTrackId } = useMusicPlayer(); const { isConnected, serverConnected } = useNetworkStatus(); const haptic = useHaptic("light"); const imageUrl = useMemo(() => { const albumId = track.AlbumId || track.ParentId; if (albumId) { return `${api?.basePath}/Items/${albumId}/Images/Primary?maxHeight=100&maxWidth=100`; } return getPrimaryImageUrl({ api, item: track }); }, [api, track]); const isCurrentTrack = currentTrack?.Id === track.Id; const isTrackLoading = loadingTrackId === track.Id; // Track download status with reactivity to completion events const [downloadStatus, setDownloadStatus] = useState< "none" | "downloading" | "downloaded" | "cached" >(() => { if (isPermanentlyDownloaded(track.Id)) return "downloaded"; if (isPermanentDownloading(track.Id)) return "downloading"; if (isCached(track.Id)) return "cached"; return "none"; }); // Listen for download completion/error events useEffect(() => { const onComplete = (event: { itemId: string; permanent: boolean }) => { if (event.itemId === track.Id) { setDownloadStatus(event.permanent ? "downloaded" : "cached"); } }; const onError = (event: { itemId: string }) => { if (event.itemId === track.Id) { setDownloadStatus("none"); } }; audioStorageEvents.on("complete", onComplete); audioStorageEvents.on("error", onError); return () => { audioStorageEvents.off("complete", onComplete); audioStorageEvents.off("error", onError); }; }, [track.Id]); // Re-check status when track changes (for list item recycling) useEffect(() => { if (isPermanentlyDownloaded(track.Id)) { setDownloadStatus("downloaded"); } else if (isPermanentDownloading(track.Id)) { setDownloadStatus("downloading"); } else if (isCached(track.Id)) { setDownloadStatus("cached"); } else { setDownloadStatus("none"); } }, [track.Id]); const _isDownloaded = downloadStatus === "downloaded"; // Check if available locally (either cached or permanently downloaded) const isAvailableLocally = !!getLocalPath(track.Id); // Consider offline if either no network connection OR server is unreachable const isOffline = !isConnected || serverConnected === false; const isUnavailableOffline = isOffline && !isAvailableLocally; const duration = useMemo(() => { if (!track.RunTimeTicks) return ""; return formatDuration(track.RunTimeTicks); }, [track.RunTimeTicks]); const handlePress = useCallback(() => { if (isUnavailableOffline) return; playTrack(track, queue); }, [playTrack, track, queue, isUnavailableOffline]); const handleLongPress = useCallback(() => { onOptionsPress?.(track); }, [onOptionsPress, track]); const handleOptionsPress = useCallback(() => { haptic(); onOptionsPress?.(track); }, [haptic, onOptionsPress, track]); return ( {/* Album artwork */} {showArtwork && ( {imageUrl ? ( ) : ( )} {isTrackLoading && ( )} )} {/* Track info */} {isCurrentTrack && isPlaying && } {track.Name} {track.Artists?.join(", ") || track.AlbumArtist} {/* Download/cache status indicator */} {downloadStatus === "downloading" && ( )} {downloadStatus === "downloaded" && ( )} {/* Duration */} {duration} {/* Options button */} {onOptionsPress && ( )} ); };