import { Text } from "@/components/common/Text"; import { useDownload } from "@/providers/DownloadProvider"; import { apiAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { JobStatus } from "@/utils/optimize-server"; import { formatTimeString } from "@/utils/time"; import { Ionicons } from "@expo/vector-icons"; import { checkForExistingDownloads } from "@kesha-antonov/react-native-background-downloader"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useRouter } from "expo-router"; import { FFmpegKit } from "ffmpeg-kit-react-native"; import { useAtom } from "jotai"; import { ActivityIndicator, TouchableOpacity, TouchableOpacityProps, View, ViewProps, } from "react-native"; import { toast } from "sonner-native"; import { Button } from "../Button"; import { Image } from "expo-image"; import { useMemo } from "react"; import { storage } from "@/utils/mmkv"; interface Props extends ViewProps {} export const ActiveDownloads: React.FC = ({ ...props }) => { const { processes, startDownload } = useDownload(); if (processes?.length === 0) return ( Active download No active downloads ); return ( Active downloads {processes?.map((p) => ( ))} ); }; interface DownloadCardProps extends TouchableOpacityProps { process: JobStatus; } const DownloadCard = ({ process, ...props }: DownloadCardProps) => { const { processes, startDownload } = useDownload(); const router = useRouter(); const { removeProcess, setProcesses } = useDownload(); const [settings] = useSettings(); const queryClient = useQueryClient(); const cancelJobMutation = useMutation({ mutationFn: async (id: string) => { if (!process) throw new Error("No active download"); if (settings?.downloadMethod === "optimized") { try { const tasks = await checkForExistingDownloads(); for (const task of tasks) { if (task.id === id) { task.stop(); } } } catch (e) { throw e; } finally { await removeProcess(id); await queryClient.refetchQueries({ queryKey: ["jobs"] }); } } else { FFmpegKit.cancel(); setProcesses((prev) => prev.filter((p) => p.id !== id)); } }, onSuccess: () => { toast.success("Download canceled"); }, onError: (e) => { console.error(e); toast.error("Could not cancel download"); }, }); const eta = (p: JobStatus) => { if (!p.speed || !p.progress) return null; const length = p?.item?.RunTimeTicks || 0; const timeLeft = (length - length * (p.progress / 100)) / p.speed; return formatTimeString(timeLeft, true); }; const base64Image = useMemo(() => { return storage.getString(process.item.Id!); }, []); return ( router.push(`/(auth)/items/page?id=${process.item.Id}`)} className="relative bg-neutral-900 border border-neutral-800 rounded-2xl overflow-hidden" {...props} > {(process.status === "optimizing" || process.status === "downloading") && ( )} {base64Image && ( )} {process.item.Type} {process.item.Name} {process.item.ProductionYear} {process.progress === 0 ? ( ) : ( {process.progress.toFixed(0)}% )} {process.speed && ( {process.speed?.toFixed(2)}x )} {eta(process) && ( ETA {eta(process)} )} {process.status} cancelJobMutation.mutate(process.id)} className="ml-auto" > {cancelJobMutation.isPending ? ( ) : ( )} {process.status === "completed" && ( )} ); };