diff --git a/components/downloads/ActiveDownloads.tsx b/components/downloads/ActiveDownloads.tsx index 178296b1..97b197c4 100644 --- a/components/downloads/ActiveDownloads.tsx +++ b/components/downloads/ActiveDownloads.tsx @@ -1,27 +1,9 @@ -import { Ionicons } from "@expo/vector-icons"; -import { 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, - TouchableOpacity, - type TouchableOpacityProps, - View, - type ViewProps, -} from "react-native"; -import { toast } from "sonner-native"; +import { View, type ViewProps } from "react-native"; import { Text } from "@/components/common/Text"; import { useDownload } from "@/providers/DownloadProvider"; import { JobStatus } from "@/providers/Downloads/types"; -import { storage } from "@/utils/mmkv"; -import { formatTimeString } from "@/utils/time"; -import { Button } from "../Button"; - -const bytesToMB = (bytes: number) => { - return bytes / 1024 / 1024; -}; +import { DownloadCard } from "./DownloadCard"; interface ActiveDownloadsProps extends ViewProps {} @@ -52,163 +34,3 @@ export default function ActiveDownloads({ ...props }: ActiveDownloadsProps) { ); } - -interface DownloadCardProps extends TouchableOpacityProps { - process: JobStatus; -} - -const DownloadCard = ({ process, ...props }: DownloadCardProps) => { - const { startDownload, pauseDownload, resumeDownload, removeProcess } = - useDownload(); - const router = useRouter(); - const queryClient = useQueryClient(); - - const handlePause = async (id: string) => { - try { - await pauseDownload(id); - toast.success(t("home.downloads.toasts.download_paused")); - } catch (error) { - console.error("Error pausing download:", error); - toast.error(t("home.downloads.toasts.could_not_pause_download")); - } - }; - - const handleResume = async (id: string) => { - try { - await resumeDownload(id); - toast.success(t("home.downloads.toasts.download_resumed")); - } catch (error) { - console.error("Error resuming download:", error); - toast.error(t("home.downloads.toasts.could_not_resume_download")); - } - }; - - const handleDelete = async (id: string) => { - try { - await removeProcess(id); - toast.success(t("home.downloads.toasts.download_deleted")); - queryClient.invalidateQueries({ queryKey: ["downloads"] }); - } catch (error) { - console.error("Error deleting download:", error); - toast.error(t("home.downloads.toasts.could_not_delete_download")); - } - }; - - const eta = (p: JobStatus) => { - if (!p.speed || p.speed <= 0 || !p.estimatedTotalSizeBytes) return null; - - const bytesRemaining = p.estimatedTotalSizeBytes - (p.bytesDownloaded || 0); - if (bytesRemaining <= 0) return null; - - const secondsRemaining = bytesRemaining / p.speed; - - return formatTimeString(secondsRemaining, "s"); - }; - - 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 === "downloading" && ( - - )} - - - {base64Image && ( - - - - )} - - {process.item.Type} - {process.item.Name} - - {process.item.ProductionYear} - - - {process.progress === 0 ? ( - - ) : ( - {process.progress.toFixed(0)}% - )} - {process.speed && process.speed > 0 && ( - - {bytesToMB(process.speed).toFixed(2)} MB/s - - )} - {eta(process) && ( - - {t("home.downloads.eta", { eta: eta(process) })} - - )} - - - - {process.status} - - - - {process.status === "downloading" && ( - handlePause(process.id)} - className='p-2 rounded-full bg-yellow-600' - > - - - )} - {process.status === "paused" && ( - handleResume(process.id)} - className='p-2 rounded-full bg-green-600' - > - - - )} - handleDelete(process.id)} - className='p-2 rounded-full bg-red-600' - > - - - - - {process.status === "completed" && ( - - - - )} - - - ); -}; diff --git a/components/downloads/DownloadCard.tsx b/components/downloads/DownloadCard.tsx new file mode 100644 index 00000000..5ffce752 --- /dev/null +++ b/components/downloads/DownloadCard.tsx @@ -0,0 +1,198 @@ +import { Ionicons } from "@expo/vector-icons"; +import { 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, + TouchableOpacity, + type TouchableOpacityProps, + View, +} from "react-native"; +import { toast } from "sonner-native"; +import { Text } from "@/components/common/Text"; +import { useDownload } from "@/providers/DownloadProvider"; +import { JobStatus } from "@/providers/Downloads/types"; +import { storage } from "@/utils/mmkv"; +import { formatTimeString } from "@/utils/time"; +import { Button } from "../Button"; + +const bytesToMB = (bytes: number) => { + return bytes / 1024 / 1024; +}; + +interface DownloadCardProps extends TouchableOpacityProps { + process: JobStatus; +} + +export const DownloadCard = ({ process, ...props }: DownloadCardProps) => { + const { startDownload, pauseDownload, resumeDownload, removeProcess } = + useDownload(); + const router = useRouter(); + const queryClient = useQueryClient(); + + const handlePause = async (id: string) => { + try { + await pauseDownload(id); + toast.success(t("home.downloads.toasts.download_paused")); + } catch (error) { + console.error("Error pausing download:", error); + toast.error(t("home.downloads.toasts.could_not_pause_download")); + } + }; + + const handleResume = async (id: string) => { + try { + await resumeDownload(id); + toast.success(t("home.downloads.toasts.download_resumed")); + } catch (error) { + console.error("Error resuming download:", error); + toast.error(t("home.downloads.toasts.could_not_resume_download")); + } + }; + + const handleDelete = async (id: string) => { + try { + await removeProcess(id); + toast.success(t("home.downloads.toasts.download_deleted")); + queryClient.invalidateQueries({ queryKey: ["downloads"] }); + } catch (error) { + console.error("Error deleting download:", error); + toast.error(t("home.downloads.toasts.could_not_delete_download")); + } + }; + + const eta = (p: JobStatus) => { + if (!p.speed || p.speed <= 0 || !p.estimatedTotalSizeBytes) return null; + + const bytesRemaining = p.estimatedTotalSizeBytes - (p.bytesDownloaded || 0); + if (bytesRemaining <= 0) return null; + + const secondsRemaining = bytesRemaining / p.speed; + + return formatTimeString(secondsRemaining, "s"); + }; + + const base64Image = useMemo(() => { + return storage.getString(process.item.Id!); + }, []); + + // Sanitize progress to ensure it's within valid bounds + const sanitizedProgress = useMemo(() => { + if ( + typeof process.progress !== "number" || + Number.isNaN(process.progress) + ) { + return 0; + } + return Math.max(0, Math.min(100, process.progress)); + }, [process.progress]); + + 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 === "downloading" && ( + 0 + ? `${Math.max(5, sanitizedProgress)}%` + : "5%", + }} + /> + )} + + {/* Action buttons in top right corner */} + + {process.status === "downloading" && ( + handlePause(process.id)} + className='p-1' + > + + + )} + {process.status === "paused" && ( + handleResume(process.id)} + className='p-1' + > + + + )} + handleDelete(process.id)} + className='p-1' + > + + + + + + + {base64Image && ( + + + + )} + + {process.item.Type} + {process.item.Name} + + {process.item.ProductionYear} + + + {sanitizedProgress === 0 ? ( + + ) : ( + {sanitizedProgress.toFixed(0)}% + )} + {process.speed && process.speed > 0 && ( + + {bytesToMB(process.speed).toFixed(2)} MB/s + + )} + {eta(process) && ( + + {t("home.downloads.eta", { eta: eta(process) })} + + )} + + + + {process.status} + + + + {process.status === "completed" && ( + + + + )} + + + ); +};