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" && (
+
+
+
+ )}
+
+
+ );
+};