import { Ionicons } from "@expo/vector-icons"; import { BottomSheetBackdrop, type BottomSheetBackdropProps, BottomSheetModal, BottomSheetView, } from "@gorhom/bottom-sheet"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; import { useAtom } from "jotai"; import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { useTranslation } from "react-i18next"; import { ActivityIndicator, StyleSheet, TouchableOpacity, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; import useRouter from "@/hooks/useAppRouter"; import { useFavorite } from "@/hooks/useFavorite"; import { audioStorageEvents, deleteTrack, downloadTrack, isCached, isPermanentDownloading, isPermanentlyDownloaded, } from "@/providers/AudioStorage"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useMusicPlayer } from "@/providers/MusicPlayerProvider"; import { getAudioStreamUrl } from "@/utils/jellyfin/audio/getAudioStreamUrl"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; interface Props { open: boolean; setOpen: (open: boolean) => void; track: BaseItemDto | null; onAddToPlaylist: () => void; playlistId?: string; onRemoveFromPlaylist?: () => void; } export const TrackOptionsSheet: React.FC = ({ open, setOpen, track, onAddToPlaylist, playlistId, onRemoveFromPlaylist, }) => { const bottomSheetModalRef = useRef(null); const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const router = useRouter(); const { playNext, addToQueue } = useMusicPlayer(); const insets = useSafeAreaInsets(); const { t } = useTranslation(); const [isDownloadingTrack, setIsDownloadingTrack] = useState(false); // Counter to trigger re-evaluation of download status when storage changes const [storageUpdateCounter, setStorageUpdateCounter] = useState(0); // Listen for storage events to update download status useEffect(() => { const handleComplete = (event: { itemId: string }) => { if (event.itemId === track?.Id) { setStorageUpdateCounter((c) => c + 1); } }; audioStorageEvents.on("complete", handleComplete); return () => { audioStorageEvents.off("complete", handleComplete); }; }, [track?.Id]); // Force re-evaluation of cache status when track changes useEffect(() => { setStorageUpdateCounter((c) => c + 1); }, [track?.Id]); // Use a placeholder item for useFavorite when track is null const { isFavorite, toggleFavorite } = useFavorite( track ?? ({ Id: "", UserData: { IsFavorite: false } } as BaseItemDto), ); // Check download status (storageUpdateCounter triggers re-evaluation when download completes) const isAlreadyDownloaded = useMemo( () => isPermanentlyDownloaded(track?.Id), [track?.Id, storageUpdateCounter], ); const isOnlyCached = useMemo( () => isCached(track?.Id), [track?.Id, storageUpdateCounter], ); const isCurrentlyDownloading = useMemo( () => isPermanentDownloading(track?.Id), [track?.Id, storageUpdateCounter], ); const imageUrl = useMemo(() => { if (!track) return null; const albumId = track.AlbumId || track.ParentId; if (albumId) { return `${api?.basePath}/Items/${albumId}/Images/Primary?maxHeight=200&maxWidth=200`; } return getPrimaryImageUrl({ api, item: track }); }, [api, track]); useEffect(() => { if (open) bottomSheetModalRef.current?.present(); else bottomSheetModalRef.current?.dismiss(); }, [open]); const handleSheetChanges = useCallback( (index: number) => { if (index === -1) { setOpen(false); } }, [setOpen], ); const renderBackdrop = useCallback( (props: BottomSheetBackdropProps) => ( ), [], ); const handlePlayNext = useCallback(() => { if (track) { playNext(track); setOpen(false); } }, [track, playNext, setOpen]); const handleAddToQueue = useCallback(() => { if (track) { addToQueue(track); setOpen(false); } }, [track, addToQueue, setOpen]); const handleAddToPlaylist = useCallback(() => { setOpen(false); setTimeout(() => { onAddToPlaylist(); }, 300); }, [onAddToPlaylist, setOpen]); const handleRemoveFromPlaylist = useCallback(() => { if (onRemoveFromPlaylist) { onRemoveFromPlaylist(); setOpen(false); } }, [onRemoveFromPlaylist, setOpen]); const handleDownload = useCallback(async () => { if (!track?.Id || !api || !user?.Id || isAlreadyDownloaded) return; setIsDownloadingTrack(true); try { const result = await getAudioStreamUrl(api, user.Id, track.Id); if (result?.url && !result.isTranscoding) { await downloadTrack(track.Id, result.url, { permanent: true, container: result.mediaSource?.Container || undefined, }); } } catch { // Silent fail } setIsDownloadingTrack(false); setOpen(false); }, [track?.Id, api, user?.Id, isAlreadyDownloaded, setOpen]); const handleDelete = useCallback(async () => { if (!track?.Id) return; await deleteTrack(track.Id); setStorageUpdateCounter((c) => c + 1); setOpen(false); }, [track?.Id, setOpen]); const handleGoToArtist = useCallback(() => { const artistId = track?.ArtistItems?.[0]?.Id; if (artistId) { setOpen(false); router.push({ pathname: "/music/artist/[artistId]", params: { artistId }, }); } }, [track?.ArtistItems, router, setOpen]); const handleGoToAlbum = useCallback(() => { const albumId = track?.AlbumId || track?.ParentId; if (albumId) { setOpen(false); router.push({ pathname: "/music/album/[albumId]", params: { albumId }, }); } }, [track?.AlbumId, track?.ParentId, router, setOpen]); const handleToggleFavorite = useCallback(() => { if (track) { toggleFavorite(); setOpen(false); } }, [track, toggleFavorite, setOpen]); // Check if navigation options are available const hasArtist = !!track?.ArtistItems?.[0]?.Id; const hasAlbum = !!(track?.AlbumId || track?.ParentId); if (!track) return null; return ( {/* Track Info Header */} {imageUrl ? ( ) : ( )} {track.Name} {track.Artists?.join(", ") || track.AlbumArtist} {/* Playback Options */} {t("music.track_options.play_next")} {t("music.track_options.add_to_queue")} {/* Library Options */} {isFavorite ? t("music.track_options.remove_from_favorites") : t("music.track_options.add_to_favorites")} {t("music.track_options.add_to_playlist")} {playlistId && ( <> {t("music.track_options.remove_from_playlist")} )} {isCurrentlyDownloading || isDownloadingTrack ? ( ) : ( )} {isCurrentlyDownloading || isDownloadingTrack ? t("music.track_options.downloading") : isAlreadyDownloaded ? t("music.track_options.downloaded") : t("music.track_options.download")} {isOnlyCached && !isAlreadyDownloaded && ( <> {t("music.track_options.cached")} )} {(isAlreadyDownloaded || isOnlyCached) && ( <> {isAlreadyDownloaded ? t("music.track_options.delete_download") : t("music.track_options.delete_cache")} )} {/* Navigation Options */} {(hasArtist || hasAlbum) && ( {hasArtist && ( <> {t("music.track_options.go_to_artist")} {hasAlbum && } )} {hasAlbum && ( {t("music.track_options.go_to_album")} )} )} ); }; const styles = StyleSheet.create({ separator: { height: StyleSheet.hairlineWidth, backgroundColor: "#404040", }, });