import * as Application from "expo-application"; import { Directory, Paths } from "expo-file-system"; import { atom, useAtom } from "jotai"; import { createContext, useCallback, useContext, useMemo, useRef } from "react"; import { Platform } from "react-native"; import { useHaptic } from "@/hooks/useHaptic"; import { getAllDownloadedItems, getDownloadedItemById, getDownloadsDatabase, updateDownloadedItem, } from "./Downloads/database"; import { getDownloadedItemSize } from "./Downloads/fileOperations"; import { useDownloadEventHandlers } from "./Downloads/hooks/useDownloadEventHandlers"; import { useDownloadOperations } from "./Downloads/hooks/useDownloadOperations"; import type { JobStatus } from "./Downloads/types"; import { apiAtom } from "./JellyfinProvider"; export const processesAtom = atom([]); export const downloadsRefreshAtom = atom(0); const DownloadContext = createContext | null>(null); function useDownloadProvider() { const [api] = useAtom(apiAtom); const [processes, setProcesses] = useAtom(processesAtom); const [refreshKey, setRefreshKey] = useAtom(downloadsRefreshAtom); const successHapticFeedback = useHaptic("success"); // Track task ID to process ID mapping const taskMapRef = useRef>(new Map()); // Reactive downloaded items that updates when refreshKey changes const downloadedItems = useMemo(() => { return getAllDownloadedItems(); }, [refreshKey]); // Trigger refresh of download lists const triggerRefresh = useCallback(() => { setRefreshKey((prev) => prev + 1); }, [setRefreshKey]); const authHeader = useMemo(() => { return api?.accessToken; }, [api]); const APP_CACHE_DOWNLOAD_DIRECTORY = new Directory( Paths.cache, `${Application.applicationId}/Downloads/`, ); const updateProcess = useCallback( ( processId: string, updater: | Partial | ((current: JobStatus) => Partial), ) => { 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) => { // 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); } }); }, 0); }, [setProcesses], ); // Set up download event handlers useDownloadEventHandlers({ taskMapRef, processes, updateProcess, removeProcess, onSuccess: successHapticFeedback, onDataChange: triggerRefresh, api: api || undefined, }); // Get download operation functions const { startBackgroundDownload, cancelDownload, deleteFile, deleteItems, deleteAllFiles, deleteFileByType, appSizeUsage, } = useDownloadOperations({ taskMapRef, processes, setProcesses, removeProcess, api, authHeader, onDataChange: triggerRefresh, }); return { processes, startBackgroundDownload, downloadedItems, // Reactive value that auto-updates getDownloadedItems: getAllDownloadedItems, // Keep for backward compatibility getDownloadsDatabase, deleteAllFiles, deleteFile, deleteItems, deleteFileByType, removeProcess, cancelDownload, getDownloadedItemSize, getDownloadedItemById, updateDownloadedItem, triggerRefresh, APP_CACHE_DOWNLOAD_DIRECTORY: APP_CACHE_DOWNLOAD_DIRECTORY.uri, appSizeUsage, // Deprecated/not implemented in simple version startDownload: async () => {}, cleanCacheDirectory: async () => {}, dumpDownloadDiagnostics: async () => "", }; } export function useDownload() { const context = useContext(DownloadContext); if (Platform.isTV) { return { processes: [], startBackgroundDownload: async () => {}, downloadedItems: [], getDownloadedItems: () => [], getDownloadsDatabase: () => ({ movies: {}, series: {}, other: {} }), deleteAllFiles: async () => {}, deleteFile: async () => {}, deleteItems: async () => {}, deleteFileByType: async () => {}, removeProcess: () => {}, cancelDownload: async () => {}, triggerRefresh: () => {}, startDownload: async () => {}, getDownloadedItemSize: () => 0, getDownloadedItemById: () => undefined, updateDownloadedItem: () => {}, APP_CACHE_DOWNLOAD_DIRECTORY: "", cleanCacheDirectory: async () => {}, appSizeUsage: async () => ({ total: 0, remaining: 0, appSize: 0 }), dumpDownloadDiagnostics: async () => "", }; } if (context === null) { throw new Error("useDownload must be used within a DownloadProvider"); } return context; } export function DownloadProvider({ children }: { children: React.ReactNode }) { const downloadUtils = useDownloadProvider(); return ( {children} ); }