mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
200 lines
5.6 KiB
TypeScript
200 lines
5.6 KiB
TypeScript
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<JobStatus[]>([]);
|
|
export const downloadsRefreshAtom = atom<number>(0);
|
|
|
|
const DownloadContext = createContext<ReturnType<
|
|
typeof useDownloadProvider
|
|
> | null>(null);
|
|
|
|
function useDownloadProvider() {
|
|
const [api] = useAtom(apiAtom);
|
|
const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
|
|
const [refreshKey, setRefreshKey] = useAtom(downloadsRefreshAtom);
|
|
const successHapticFeedback = useHaptic("success");
|
|
|
|
// Track task ID to process ID mapping
|
|
const taskMapRef = useRef<Map<number | string, string>>(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<JobStatus>
|
|
| ((current: JobStatus) => Partial<JobStatus>),
|
|
) => {
|
|
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 (
|
|
<DownloadContext.Provider value={downloadUtils}>
|
|
{children}
|
|
</DownloadContext.Provider>
|
|
);
|
|
}
|