diff --git a/src/contexts/DownloadsContext.tsx b/src/contexts/DownloadsContext.tsx index cb78d250..7f752a5e 100644 --- a/src/contexts/DownloadsContext.tsx +++ b/src/contexts/DownloadsContext.tsx @@ -8,7 +8,7 @@ export type DownloadStatus = 'downloading' | 'completed' | 'paused' | 'error' | export interface DownloadItem { id: string; // unique id for this download (content id + episode if any) - contentId: string; // base id + contentId: string; // base id (e.g., tt0903747 for series, tt0499549 for movies) type: 'movie' | 'series'; title: string; // movie title or show name providerName?: string; @@ -29,10 +29,13 @@ export interface DownloadItem { fileUri?: string; // local file uri once downloading/finished createdAt: number; updatedAt: number; + // Additional metadata for progress tracking + imdbId?: string; // IMDb ID for better tracking + tmdbId?: number; // TMDB ID if available } type StartDownloadInput = { - id: string; + id: string; // Base content ID (e.g., tt0903747) type: 'movie' | 'series'; title: string; providerName?: string; @@ -43,6 +46,9 @@ type StartDownloadInput = { posterUrl?: string | null; url: string; headers?: Record; + // Additional metadata for progress tracking + imdbId?: string; + tmdbId?: number; }; type DownloadsContextValue = { @@ -153,6 +159,9 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi fileUri: d.fileUri ? String(d.fileUri) : undefined, createdAt: typeof d.createdAt === 'number' ? d.createdAt : Date.now(), updatedAt: typeof d.updatedAt === 'number' ? d.updatedAt : Date.now(), + // Restore metadata for progress tracking + imdbId: (d as any).imdbId ? String((d as any).imdbId) : undefined, + tmdbId: typeof (d as any).tmdbId === 'number' ? (d as any).tmdbId : undefined, }; return safe; }); @@ -394,6 +403,9 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi fileUri, createdAt, updatedAt: createdAt, + // Store metadata for progress tracking + imdbId: input.imdbId, + tmdbId: input.tmdbId, }; setDownloads(prev => [newItem, ...prev]); diff --git a/src/screens/DownloadsScreen.tsx b/src/screens/DownloadsScreen.tsx index a4a1bdee..122942f4 100644 --- a/src/screens/DownloadsScreen.tsx +++ b/src/screens/DownloadsScreen.tsx @@ -105,11 +105,11 @@ const DownloadItemComponent: React.FC<{ case 'downloading': return currentTheme.colors.primary; case 'completed': - return currentTheme.colors.success; + return currentTheme.colors.success || '#4CAF50'; case 'paused': - return currentTheme.colors.warning; + return currentTheme.colors.warning || '#FF9500'; case 'error': - return currentTheme.colors.error; + return currentTheme.colors.error || '#FF3B30'; case 'queued': return currentTheme.colors.mediumEmphasis; default: @@ -166,7 +166,7 @@ const DownloadItemComponent: React.FC<{ return ( onPress(item)} onLongPress={handleLongPress} activeOpacity={0.8} @@ -192,7 +192,7 @@ const DownloadItemComponent: React.FC<{ {/* Provider + quality row */} - {(item.providerName || 'Provider') + (item.quality ? ` ${item.quality}` : '')} + {item.providerName || 'Provider'} {/* Status row */} @@ -344,6 +344,11 @@ const DownloadsScreen: React.FC = () => { const isMp4 = /\.mp4(\?|$)/i.test(lower); const videoType = isM3u8 ? 'm3u8' : isMpd ? 'mpd' : isMp4 ? 'mp4' : undefined; + // Build episodeId for series progress tracking (format: contentId:season:episode) + const episodeId = item.type === 'series' && item.season && item.episode + ? `${item.contentId}:${item.season}:${item.episode}` + : undefined; + const playerRoute = Platform.OS === 'ios' ? 'PlayerIOS' : 'PlayerAndroid'; navigation.navigate(playerRoute as any, { uri, @@ -357,10 +362,10 @@ const DownloadsScreen: React.FC = () => { streamName: item.providerName || 'Offline', headers: undefined, forceVlc: Platform.OS === 'android' ? isMkv : false, - id: item.id, + id: item.contentId, // Use contentId (base ID) instead of compound id for progress tracking type: item.type, - episodeId: undefined, - imdbId: undefined, + episodeId: episodeId, // Pass episodeId for series progress tracking + imdbId: (item as any).imdbId || item.contentId, // Use imdbId if available, fallback to contentId availableStreams: {}, backdrop: undefined, videoType, @@ -409,7 +414,7 @@ const DownloadsScreen: React.FC = () => { styles.filterButtonText, { color: selectedFilter === filter - ? currentTheme.colors.background + ? currentTheme.colors.white : currentTheme.colors.text, } ]}> @@ -420,7 +425,7 @@ const DownloadsScreen: React.FC = () => { styles.filterBadge, { backgroundColor: selectedFilter === filter - ? currentTheme.colors.background + ? currentTheme.colors.white : currentTheme.colors.primary, } ]}> @@ -429,7 +434,7 @@ const DownloadsScreen: React.FC = () => { { color: selectedFilter === filter ? currentTheme.colors.primary - : currentTheme.colors.background, + : currentTheme.colors.white, } ]}> {count} @@ -440,7 +445,7 @@ const DownloadsScreen: React.FC = () => { ); return ( - + { @@ -484,6 +490,7 @@ const DownloadsScreen: React.FC = () => { onAction={handleDownloadAction} /> )} + style={{ backgroundColor: currentTheme.colors.darkBackground }} contentContainerStyle={styles.listContainer} showsVerticalScrollIndicator={false} refreshControl={ @@ -523,7 +530,6 @@ const styles = StyleSheet.create({ paddingHorizontal: 20, paddingBottom: 16, borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: 'rgba(255, 255, 255, 0.1)', }, headerTitle: { fontSize: 32, diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 3fbde6cb..13080b4a 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -203,7 +203,7 @@ const AnimatedView = memo(({ }); // Extracted Components -const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, theme, showLogos, scraperLogo, showAlert, parentTitle, parentType, parentSeason, parentEpisode, parentEpisodeTitle, parentPosterUrl, providerName }: { +const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, theme, showLogos, scraperLogo, showAlert, parentTitle, parentType, parentSeason, parentEpisode, parentEpisodeTitle, parentPosterUrl, providerName, parentId, parentImdbId }: { stream: Stream; onPress: () => void; index: number; @@ -220,6 +220,8 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the parentEpisodeTitle?: string; parentPosterUrl?: string | null; providerName?: string; + parentId?: string; // Content ID (e.g., tt0903747 or tmdb:1396) + parentImdbId?: string; // IMDb ID if available }) => { const { useSettings } = require('../hooks/useSettings'); const { settings } = useSettings(); @@ -301,7 +303,17 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the const episodeTitle = parentEpisodeTitle || parent.episodeTitle || parent.episode_name; // Prefer the stream's display name (often includes provider + resolution) const provider = (stream.name as any) || (stream.title as any) || providerName || parent.addonName || parent.addonId || (stream.addonName as any) || (stream.addonId as any) || 'Provider'; - const idForContent = parent.imdbId || parent.tmdbId || parent.addonId || inferredTitle; + + // Use parentId first (from route params), fallback to stream metadata + const idForContent = parentId || parent.imdbId || parent.tmdbId || parent.addonId || inferredTitle; + + // Extract tmdbId if available (from parentId or parent metadata) + let tmdbId: number | undefined = undefined; + if (parentId && parentId.startsWith('tmdb:')) { + tmdbId = parseInt(parentId.split(':')[1], 10); + } else if (typeof parent.tmdbId === 'number') { + tmdbId = parent.tmdbId; + } await startDownload({ id: String(idForContent), @@ -315,10 +327,13 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the posterUrl: parentPosterUrl || parent.poster || parent.backdrop || null, url, headers: (stream.headers as any) || undefined, + // Pass metadata for progress tracking + imdbId: parentImdbId || parent.imdbId || undefined, + tmdbId: tmdbId, }); Toast.success('Download started', 'bottom'); } catch {} - }, [startDownload, stream.url, stream.headers, streamInfo.quality, showAlert, stream.name, stream.title]); + }, [startDownload, stream.url, stream.headers, streamInfo.quality, showAlert, stream.name, stream.title, parentId, parentImdbId, parentTitle, parentType, parentSeason, parentEpisode, parentEpisodeTitle, parentPosterUrl, providerName]); const isDebrid = streamInfo.isDebrid; return ( @@ -2188,6 +2203,8 @@ export const StreamsScreen = () => { parentEpisodeTitle={(type === 'series' || type === 'other') ? currentEpisode?.name : undefined} parentPosterUrl={episodeImage || metadata?.poster || undefined} providerName={streams && Object.keys(streams).find(pid => (streams as any)[pid]?.streams?.includes?.(item))} + parentId={id} + parentImdbId={imdbId} /> )}