diff --git a/App.tsx b/App.tsx
index e59370abf..038fb007f 100644
--- a/App.tsx
+++ b/App.tsx
@@ -27,6 +27,7 @@ import { GenreProvider } from './src/contexts/GenreContext';
import { TraktProvider } from './src/contexts/TraktContext';
import { ThemeProvider, useTheme } from './src/contexts/ThemeContext';
import { TrailerProvider } from './src/contexts/TrailerContext';
+import { DownloadsProvider } from './src/contexts/DownloadsContext';
import SplashScreen from './src/components/SplashScreen';
import UpdatePopup from './src/components/UpdatePopup';
import MajorUpdateOverlay from './src/components/MajorUpdateOverlay';
@@ -156,29 +157,31 @@ const ThemedApp = () => {
theme={customNavigationTheme}
linking={undefined}
>
-
-
- {!isAppReady && }
- {shouldShowApp && }
- {Platform.OS === 'ios' && (
-
+
+
+ {!isAppReady && }
+ {shouldShowApp && }
+ {Platform.OS === 'ios' && (
+
+ )}
+
- )}
-
-
+
+
diff --git a/src/contexts/DownloadsContext.tsx b/src/contexts/DownloadsContext.tsx
new file mode 100644
index 000000000..e5e1c7f55
--- /dev/null
+++ b/src/contexts/DownloadsContext.tsx
@@ -0,0 +1,337 @@
+import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
+import * as FileSystem from 'expo-file-system';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+export type DownloadStatus = 'downloading' | 'completed' | 'paused' | 'error' | 'queued';
+
+export interface DownloadItem {
+ id: string; // unique id for this download (content id + episode if any)
+ contentId: string; // base id
+ type: 'movie' | 'series';
+ title: string; // movie title or show name
+ providerName?: string;
+ season?: number;
+ episode?: number;
+ episodeTitle?: string;
+ quality?: string;
+ size?: number; // total bytes if known
+ downloadedBytes: number;
+ totalBytes: number;
+ progress: number; // 0-100
+ status: DownloadStatus;
+ speedBps?: number;
+ etaSeconds?: number;
+ posterUrl?: string | null;
+ sourceUrl: string; // stream url
+ headers?: Record;
+ fileUri?: string; // local file uri once downloading/finished
+ createdAt: number;
+ updatedAt: number;
+}
+
+type StartDownloadInput = {
+ id: string;
+ type: 'movie' | 'series';
+ title: string;
+ providerName?: string;
+ season?: number;
+ episode?: number;
+ episodeTitle?: string;
+ quality?: string;
+ posterUrl?: string | null;
+ url: string;
+ headers?: Record;
+};
+
+type DownloadsContextValue = {
+ downloads: DownloadItem[];
+ startDownload: (input: StartDownloadInput) => Promise;
+ pauseDownload: (id: string) => Promise;
+ resumeDownload: (id: string) => Promise;
+ cancelDownload: (id: string) => Promise;
+ removeDownload: (id: string) => Promise;
+};
+
+const DownloadsContext = createContext(undefined);
+
+const STORAGE_KEY = 'downloads_state_v1';
+
+function sanitizeFilename(name: string): string {
+ return name.replace(/[^a-z0-9\-_.()\s]/gi, '_').slice(0, 120).trim();
+}
+
+function getExtensionFromUrl(url: string): string {
+ const lower = url.toLowerCase();
+ if (/(\.|ext=)(m3u8)(\b|$)/i.test(lower)) return 'm3u8';
+ if (/(\.|ext=)(mp4)(\b|$)/i.test(lower)) return 'mp4';
+ if (/(\.|ext=)(mkv)(\b|$)/i.test(lower)) return 'mkv';
+ if (/(\.|ext=)(mpd)(\b|$)/i.test(lower)) return 'mpd';
+ return 'mp4';
+}
+
+export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [downloads, setDownloads] = useState([]);
+ // Keep active resumables in memory (not persisted)
+ const resumablesRef = useRef