forked from Ninjalama/streamyfin_mirror
288 lines
8.8 KiB
TypeScript
288 lines
8.8 KiB
TypeScript
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<NativeDownloadButton> = ({
|
|
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<number>(-1);
|
|
const [selectedSubtitleStream, setSelectedSubtitleStream] =
|
|
useState<number>(0);
|
|
const [maxBitrate, setMaxBitrate] = useState<Bitrate>(
|
|
settings?.defaultBitrate ?? {
|
|
key: "Max",
|
|
value: undefined,
|
|
}
|
|
);
|
|
|
|
const userCanDownload = useMemo(
|
|
() => user?.Policy?.EnableContentDownloading,
|
|
[user]
|
|
);
|
|
const usingOptimizedServer = useMemo(
|
|
() => settings?.downloadMethod === DownloadMethod.Optimized,
|
|
[settings]
|
|
);
|
|
|
|
const bottomSheetModalRef = useRef<BottomSheetModal>(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) => (
|
|
<BottomSheetBackdrop
|
|
{...props}
|
|
disappearsOnIndex={-1}
|
|
appearsOnIndex={0}
|
|
/>
|
|
),
|
|
[]
|
|
);
|
|
|
|
const router = useRouter();
|
|
|
|
const activeDownload = item.Id ? downloads[item.Id] : undefined;
|
|
|
|
return (
|
|
<View {...props}>
|
|
<RoundButton
|
|
disabled={userCanDownload === false || activeDownload !== undefined}
|
|
size={size}
|
|
onPress={handlePresentModalPress}
|
|
>
|
|
{activeDownload ? (
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
router.push(`/downloads`);
|
|
}}
|
|
>
|
|
{activeDownload.state === "PENDING" && (
|
|
<ActivityIndicator size="small" color="white" />
|
|
)}
|
|
{activeDownload.state === "DOWNLOADING" && (
|
|
<ProgressCircle
|
|
size={24}
|
|
fill={activeDownload.progress * 100}
|
|
width={4}
|
|
tintColor="#9334E9"
|
|
backgroundColor="#bdc3c7"
|
|
/>
|
|
)}
|
|
{activeDownload.state === "FAILED" && (
|
|
<Ionicons name="close" size={24} color="white" />
|
|
)}
|
|
{activeDownload.state === "PAUSED" && (
|
|
<Ionicons name="pause" size={24} color="white" />
|
|
)}
|
|
{activeDownload.state === "STOPPED" && (
|
|
<Ionicons name="stop" size={24} color="white" />
|
|
)}
|
|
{activeDownload.state === "DONE" && (
|
|
<Ionicons name="cloud-done-outline" size={24} color={"white"} />
|
|
)}
|
|
</TouchableOpacity>
|
|
) : (
|
|
<Ionicons name="cloud-download-outline" size={24} color="white" />
|
|
)}
|
|
</RoundButton>
|
|
<BottomSheetModal
|
|
ref={bottomSheetModalRef}
|
|
enableDynamicSizing
|
|
handleIndicatorStyle={{
|
|
backgroundColor: "white",
|
|
}}
|
|
backgroundStyle={{
|
|
backgroundColor: "#171717",
|
|
}}
|
|
onChange={handleSheetChanges}
|
|
backdropComponent={renderBackdrop}
|
|
>
|
|
<BottomSheetView>
|
|
<View className="flex flex-col space-y-4 px-4 pb-8 pt-2">
|
|
<View>
|
|
<Text className="font-bold text-2xl text-neutral-100">
|
|
{title}
|
|
</Text>
|
|
</View>
|
|
<View className="flex flex-col space-y-2 w-full items-start">
|
|
<BitrateSelector
|
|
inverted
|
|
onChange={setMaxBitrate}
|
|
selected={maxBitrate}
|
|
/>
|
|
<MediaSourceSelector
|
|
item={item}
|
|
onChange={setSelectedMediaSource}
|
|
selected={selectedMediaSource}
|
|
/>
|
|
{selectedMediaSource && (
|
|
<View className="flex flex-col space-y-2">
|
|
<AudioTrackSelector
|
|
source={selectedMediaSource}
|
|
onChange={setSelectedAudioStream}
|
|
selected={selectedAudioStream}
|
|
/>
|
|
<SubtitleTrackSelector
|
|
source={selectedMediaSource}
|
|
onChange={setSelectedSubtitleStream}
|
|
selected={selectedSubtitleStream}
|
|
/>
|
|
</View>
|
|
)}
|
|
</View>
|
|
<Button
|
|
className="mt-auto"
|
|
onPress={acceptDownloadOptions}
|
|
color="purple"
|
|
>
|
|
{t("item_card.download.download_button")}
|
|
</Button>
|
|
<View className="opacity-70 text-center w-full flex items-center">
|
|
<Text className="text-xs">
|
|
{usingOptimizedServer
|
|
? t("item_card.download.using_optimized_server")
|
|
: t("item_card.download.using_default_method")}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</BottomSheetView>
|
|
</BottomSheetModal>
|
|
</View>
|
|
);
|
|
};
|