import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useNativeDownloads } from "@/providers/NativeDownloadProvider"; import { DownloadMethod, useSettings } from "@/utils/atoms/settings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import download from "@/utils/profiles/download"; import Ionicons from "@expo/vector-icons/Ionicons"; import { BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModal, BottomSheetView, } from "@gorhom/bottom-sheet"; import { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client/models"; import { useFocusEffect, useRouter } from "expo-router"; import { t } from "i18next"; import { useAtom } from "jotai"; import React, { useCallback, useMemo, useRef, useState } from "react"; import { ActivityIndicator, TouchableOpacity, View, ViewProps, } from "react-native"; import { toast } from "sonner-native"; import { AudioTrackSelector } from "../AudioTrackSelector"; import { Bitrate, BitrateSelector } from "../BitrateSelector"; import { Button } from "../Button"; import { Text } from "../common/Text"; import { MediaSourceSelector } from "../MediaSourceSelector"; import ProgressCircle from "../ProgressCircle"; import { RoundButton } from "../RoundButton"; import { SubtitleTrackSelector } from "../SubtitleTrackSelector"; interface NativeDownloadButton extends ViewProps { item: BaseItemDto; title?: string; subtitle?: string; size?: "default" | "large"; } export const NativeDownloadButton: React.FC = ({ item, title = "Download", subtitle = "", size = "default", ...props }) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const [settings] = useSettings(); const { downloads, startDownload } = useNativeDownloads(); const [selectedMediaSource, setSelectedMediaSource] = useState< MediaSourceInfo | undefined | null >(undefined); const [selectedAudioStream, setSelectedAudioStream] = useState(-1); const [selectedSubtitleStream, setSelectedSubtitleStream] = useState(0); const [maxBitrate, setMaxBitrate] = useState( settings?.defaultBitrate ?? { key: "Max", value: undefined, } ); const userCanDownload = useMemo( () => user?.Policy?.EnableContentDownloading, [user] ); const usingOptimizedServer = useMemo( () => settings?.downloadMethod === DownloadMethod.Optimized, [settings] ); const bottomSheetModalRef = useRef(null); const handlePresentModalPress = useCallback(() => { bottomSheetModalRef.current?.present(); }, []); const handleSheetChanges = useCallback((index: number) => {}, []); const closeModal = useCallback(() => { bottomSheetModalRef.current?.dismiss(); }, []); const acceptDownloadOptions = useCallback(async () => { if (userCanDownload === true) { closeModal(); try { const res = await getStreamUrl({ api, item, startTimeTicks: 0, userId: user?.Id, audioStreamIndex: selectedAudioStream, maxStreamingBitrate: maxBitrate.value, mediaSourceId: selectedMediaSource?.Id, subtitleStreamIndex: selectedSubtitleStream, deviceProfile: download, }); if (!res?.url) throw new Error("No url found"); if (!item.Id || !item.Name) throw new Error("No item id found"); if (!selectedMediaSource) throw new Error("No media source found"); if (!selectedAudioStream) throw new Error("No audio stream found"); await startDownload(item, res.url, { maxBitrate: maxBitrate.value, selectedAudioStream, selectedSubtitleStream, selectedMediaSource, }); toast.success( t("home.downloads.toasts.download_started_for", { item: item.Name }), { action: { label: "Go to download", onClick: () => { router.push("/downloads"); toast.dismiss(); }, }, } ); } catch (error) { console.error("Download error:", error); toast.error("Failed to start download"); } } else { toast.error( t("home.downloads.toasts.you_are_not_allowed_to_download_files") ); } closeModal(); }, [ userCanDownload, maxBitrate, selectedMediaSource, selectedAudioStream, selectedSubtitleStream, item, user, api, ]); useFocusEffect( useCallback(() => { if (!settings) return; const { bitrate, mediaSource, audioIndex, subtitleIndex } = getDefaultPlaySettings(item, settings); setSelectedMediaSource(mediaSource ?? undefined); setSelectedAudioStream(audioIndex ?? 0); setSelectedSubtitleStream(subtitleIndex ?? -1); setMaxBitrate(bitrate); }, [item, settings]) ); const renderBackdrop = useCallback( (props: BottomSheetBackdropProps) => ( ), [] ); const router = useRouter(); const activeDownload = item.Id ? downloads[item.Id] : undefined; return ( {activeDownload ? ( { router.push(`/downloads`); }} > {activeDownload.state === "PENDING" && ( )} {activeDownload.state === "DOWNLOADING" && ( )} {activeDownload.state === "FAILED" && ( )} {activeDownload.state === "PAUSED" && ( )} {activeDownload.state === "STOPPED" && ( )} {activeDownload.state === "DONE" && ( )} ) : ( )} {title} {selectedMediaSource && ( )} {usingOptimizedServer ? t("item_card.download.using_optimized_server") : t("item_card.download.using_default_method")} ); };