import { Text } from "@/components/common/Text"; import { useDownload } from "@/providers/DownloadProvider"; import { DownloadMethod, useSettings } from "@/utils/atoms/settings"; import { storage } from "@/utils/mmkv"; import type { JobStatus } from "@/utils/optimize-server"; import { formatTimeString } from "@/utils/time"; import { Ionicons } from "@expo/vector-icons"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useRouter } from "expo-router"; import { t } from "i18next"; import { useMemo } from "react"; import { ActivityIndicator, Platform, TouchableOpacity, type TouchableOpacityProps, View, type ViewProps, } from "react-native"; import { toast } from "sonner-native"; import { Button } from "../Button"; const BackGroundDownloader = !Platform.isTV ? require("@kesha-antonov/react-native-background-downloader") : null; const FFmpegKitProvider = !Platform.isTV ? require("ffmpeg-kit-react-native") : null; interface Props extends ViewProps {} export const ActiveDownloads: React.FC = ({ ...props }) => { const { processes } = useDownload(); if (processes?.length === 0) return ( {t("home.downloads.active_download")} {t("home.downloads.no_active_downloads")} ); return ( {t("home.downloads.active_downloads")} {processes?.map((p: JobStatus) => ( ))} ); }; 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 === DownloadMethod.Optimized) { try { const tasks = await BackGroundDownloader.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 { FFmpegKitProvider.FFmpegKit.cancel(Number(id)); setProcesses((prev: any[]) => prev.filter((p: { id: string }) => p.id !== id), ); } }, onSuccess: () => { toast.success(t("home.downloads.toasts.download_cancelled")); }, onError: (e) => { console.error(e); toast.error(t("home.downloads.toasts.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, "tick"); }; 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) && ( {t("home.downloads.eta", { eta: eta(process) })} )} {process.status} cancelJobMutation.mutate(process.id)} className='ml-auto' > {cancelJobMutation.isPending ? ( ) : ( )} {process.status === "completed" && ( )} ); };