diff --git a/components/DownloadItem.tsx b/components/DownloadItem.tsx index fe34d7c3..2f2268b6 100644 --- a/components/DownloadItem.tsx +++ b/components/DownloadItem.tsx @@ -132,13 +132,15 @@ export const DownloadItems: React.FC = ({ return itemsNotDownloaded.length === 0; }, [items, itemsNotDownloaded]); const itemsProcesses = useMemo( - () => processes?.filter((p) => itemIds.includes(p.item.Id)), + () => + processes?.filter((p) => p?.item?.Id && itemIds.includes(p.item.Id)) || + [], [processes, itemIds], ); const progress = useMemo(() => { if (itemIds.length === 1) - return itemsProcesses.reduce((acc, p) => acc + p.progress, 0); + return itemsProcesses.reduce((acc, p) => acc + (p.progress || 0), 0); return ( ((itemIds.length - queue.filter((q) => itemIds.includes(q.item.Id)).length) / @@ -262,9 +264,9 @@ export const DownloadItems: React.FC = ({ closeModal(); // Wait for modal dismiss animation to complete - requestAnimationFrame(() => { + setTimeout(() => { initiateDownload(...itemsToDownload); - }); + }, 300); } else { toast.error( t("home.downloads.toasts.you_are_not_allowed_to_download_files"), diff --git a/components/downloads/ActiveDownloads.tsx b/components/downloads/ActiveDownloads.tsx index 3dba2418..7835cdf1 100644 --- a/components/downloads/ActiveDownloads.tsx +++ b/components/downloads/ActiveDownloads.tsx @@ -9,7 +9,11 @@ interface ActiveDownloadsProps extends ViewProps {} export default function ActiveDownloads({ ...props }: ActiveDownloadsProps) { const { processes } = useDownload(); - if (processes?.length === 0) + + // Filter out any invalid processes before rendering + const validProcesses = processes?.filter((p) => p?.item?.Id) || []; + + if (validProcesses.length === 0) return ( @@ -27,8 +31,8 @@ export default function ActiveDownloads({ ...props }: ActiveDownloadsProps) { {t("home.downloads.active_downloads")} - {processes?.map((p: JobStatus) => ( - + {validProcesses.map((p: JobStatus) => ( + ))} diff --git a/components/downloads/DownloadCard.tsx b/components/downloads/DownloadCard.tsx index 3f994a16..5453f32a 100644 --- a/components/downloads/DownloadCard.tsx +++ b/components/downloads/DownloadCard.tsx @@ -51,7 +51,7 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => { }; const eta = useMemo(() => { - if (!process.estimatedTotalSizeBytes || !process.bytesDownloaded) { + if (!process?.estimatedTotalSizeBytes || !process?.bytesDownloaded) { return null; } @@ -66,13 +66,14 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => { } return formatTimeString(secondsRemaining, "s"); - }, [process.id, process.bytesDownloaded, process.estimatedTotalSizeBytes]); + }, [process?.id, process?.bytesDownloaded, process?.estimatedTotalSizeBytes]); const estimatedSize = useMemo(() => { - if (process.estimatedTotalSizeBytes) return process.estimatedTotalSizeBytes; + if (process?.estimatedTotalSizeBytes) + return process.estimatedTotalSizeBytes; // Calculate from bitrate + duration (only if bitrate value is defined) - if (process.maxBitrate.value) { + if (process?.maxBitrate?.value && process?.item?.RunTimeTicks) { return estimateDownloadSize( process.maxBitrate.value, process.item.RunTimeTicks, @@ -81,32 +82,43 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => { return undefined; }, [ - process.maxBitrate.value, - process.item.RunTimeTicks, - process.estimatedTotalSizeBytes, + process?.maxBitrate?.value, + process?.item?.RunTimeTicks, + process?.estimatedTotalSizeBytes, ]); - const isTranscoding = process.isTranscoding || false; + const isTranscoding = process?.isTranscoding || false; const downloadedAmount = useMemo(() => { - if (!process.bytesDownloaded) return null; + if (!process?.bytesDownloaded) return null; return formatBytes(process.bytesDownloaded); - }, [process.bytesDownloaded]); + }, [process?.bytesDownloaded]); const base64Image = useMemo(() => { - return storage.getString(process.item.Id!); - }, []); + try { + const itemId = process?.item?.Id; + if (!itemId) return undefined; + return storage.getString(itemId); + } catch { + return undefined; + } + }, [process?.item?.Id]); // Sanitize progress to ensure it's within valid bounds const sanitizedProgress = useMemo(() => { if ( - typeof process.progress !== "number" || + typeof process?.progress !== "number" || Number.isNaN(process.progress) ) { return 0; } return Math.max(0, Math.min(100, process.progress)); - }, [process.progress]); + }, [process?.progress]); + + // Return null after all hooks have been called + if (!process || !process.item || !process.item.Id) { + return null; + } return ( | ((current: JobStatus) => Partial), ) => { - setProcesses((prev) => - prev.map((p) => { - if (p.id !== processId) return p; - const newStatus = - typeof updater === "function" ? updater(p) : updater; - return { - ...p, - ...newStatus, - }; - }), - ); + setProcesses((prev) => { + const processIndex = prev.findIndex((p) => p.id === processId); + if (processIndex === -1) return prev; + + const currentProcess = prev[processIndex]; + if (!currentProcess) return prev; + + const newStatus = + typeof updater === "function" ? updater(currentProcess) : updater; + + // Create new array with updated process + const newProcesses = [...prev]; + newProcesses[processIndex] = { + ...currentProcess, + ...newStatus, + }; + + return newProcesses; + }); }, [setProcesses], ); const removeProcess = useCallback( (id: string) => { - setProcesses((prev) => prev.filter((process) => process.id !== id)); + // Use setTimeout to defer removal and avoid race conditions during rendering + setTimeout(() => { + setProcesses((prev) => prev.filter((process) => process.id !== id)); - // Find and remove from task map - taskMapRef.current.forEach((processId, taskId) => { - if (processId === id) { - taskMapRef.current.delete(taskId); - } - }); + // Find and remove from task map + taskMapRef.current.forEach((processId, taskId) => { + if (processId === id) { + taskMapRef.current.delete(taskId); + } + }); + }, 0); }, [setProcesses], );