mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-10 03:50:52 +00:00
changes
This commit is contained in:
parent
271126b665
commit
92973c1c7b
4 changed files with 193 additions and 92 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { AppState } from 'react-native';
|
||||||
import * as FileSystem from 'expo-file-system';
|
import * as FileSystem from 'expo-file-system';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { notificationService } from '../services/notificationService';
|
||||||
|
|
||||||
export type DownloadStatus = 'downloading' | 'completed' | 'paused' | 'error' | 'queued';
|
export type DownloadStatus = 'downloading' | 'completed' | 'paused' | 'error' | 'queued';
|
||||||
|
|
||||||
|
|
@ -46,8 +48,8 @@ type StartDownloadInput = {
|
||||||
type DownloadsContextValue = {
|
type DownloadsContextValue = {
|
||||||
downloads: DownloadItem[];
|
downloads: DownloadItem[];
|
||||||
startDownload: (input: StartDownloadInput) => Promise<void>;
|
startDownload: (input: StartDownloadInput) => Promise<void>;
|
||||||
pauseDownload: (id: string) => Promise<void>;
|
// pauseDownload: (id: string) => Promise<void>;
|
||||||
resumeDownload: (id: string) => Promise<void>;
|
// resumeDownload: (id: string) => Promise<void>;
|
||||||
cancelDownload: (id: string) => Promise<void>;
|
cancelDownload: (id: string) => Promise<void>;
|
||||||
removeDownload: (id: string) => Promise<void>;
|
removeDownload: (id: string) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
@ -117,6 +119,38 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Notifications are configured globally by notificationService
|
||||||
|
|
||||||
|
// Track app state to know foreground/background
|
||||||
|
const appStateRef = useRef<string>('active');
|
||||||
|
useEffect(() => {
|
||||||
|
const sub = AppState.addEventListener('change', (s) => {
|
||||||
|
appStateRef.current = s;
|
||||||
|
});
|
||||||
|
return () => sub.remove();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Cache last notified progress to reduce spam
|
||||||
|
const lastNotifyRef = useRef<Map<string, number>>(new Map());
|
||||||
|
|
||||||
|
const maybeNotifyProgress = useCallback(async (d: DownloadItem) => {
|
||||||
|
try {
|
||||||
|
if (appStateRef.current === 'active') return;
|
||||||
|
if (d.status !== 'downloading') return;
|
||||||
|
const prev = lastNotifyRef.current.get(d.id) ?? -1;
|
||||||
|
if (d.progress <= prev || d.progress - prev < 2) return; // notify every 2%
|
||||||
|
lastNotifyRef.current.set(d.id, d.progress);
|
||||||
|
await notificationService.notifyDownloadProgress(d.title, d.progress, d.downloadedBytes, d.totalBytes);
|
||||||
|
} catch {}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const notifyCompleted = useCallback(async (d: DownloadItem) => {
|
||||||
|
try {
|
||||||
|
if (appStateRef.current === 'active') return;
|
||||||
|
await notificationService.notifyDownloadComplete(d.title);
|
||||||
|
} catch {}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(downloads)).catch(() => {});
|
AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(downloads)).catch(() => {});
|
||||||
}, [downloads]);
|
}, [downloads]);
|
||||||
|
|
@ -197,6 +231,11 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
speedBps,
|
speedBps,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
}));
|
}));
|
||||||
|
// Fire background progress notification (throttled)
|
||||||
|
const current = downloads.find(x => x.id === compoundId);
|
||||||
|
if (current) {
|
||||||
|
maybeNotifyProgress({ ...current, downloadedBytes: totalBytesWritten, totalBytes: totalBytesExpectedToWrite || current.totalBytes, progress: totalBytesExpectedToWrite ? Math.floor((totalBytesWritten / totalBytesExpectedToWrite) * 100) : current.progress });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create resumable
|
// Create resumable
|
||||||
|
|
@ -213,6 +252,8 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
const result = await resumable.downloadAsync();
|
const result = await resumable.downloadAsync();
|
||||||
if (!result) throw new Error('Download failed');
|
if (!result) throw new Error('Download failed');
|
||||||
updateDownload(compoundId, (d) => ({ ...d, status: 'completed', progress: 100, updatedAt: Date.now(), fileUri: result.uri }));
|
updateDownload(compoundId, (d) => ({ ...d, status: 'completed', progress: 100, updatedAt: Date.now(), fileUri: result.uri }));
|
||||||
|
const done = downloads.find(x => x.id === compoundId);
|
||||||
|
if (done) notifyCompleted({ ...done, status: 'completed', progress: 100, fileUri: result.uri } as DownloadItem);
|
||||||
resumablesRef.current.delete(compoundId);
|
resumablesRef.current.delete(compoundId);
|
||||||
lastBytesRef.current.delete(compoundId);
|
lastBytesRef.current.delete(compoundId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -227,64 +268,64 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
}
|
}
|
||||||
}, [downloads, updateDownload]);
|
}, [downloads, updateDownload]);
|
||||||
|
|
||||||
const pauseDownload = useCallback(async (id: string) => {
|
// const pauseDownload = useCallback(async (id: string) => {
|
||||||
const resumable = resumablesRef.current.get(id);
|
// const resumable = resumablesRef.current.get(id);
|
||||||
if (resumable) {
|
// if (resumable) {
|
||||||
try {
|
// try {
|
||||||
await resumable.pauseAsync();
|
// await resumable.pauseAsync();
|
||||||
} catch {}
|
// } catch {}
|
||||||
}
|
// }
|
||||||
updateDownload(id, (d) => ({ ...d, status: 'paused', updatedAt: Date.now() }));
|
// updateDownload(id, (d) => ({ ...d, status: 'paused', updatedAt: Date.now() }));
|
||||||
}, [updateDownload]);
|
// }, [updateDownload]);
|
||||||
|
|
||||||
const resumeDownload = useCallback(async (id: string) => {
|
// const resumeDownload = useCallback(async (id: string) => {
|
||||||
const item = downloads.find(d => d.id === id);
|
// const item = downloads.find(d => d.id === id);
|
||||||
if (!item) return;
|
// if (!item) return;
|
||||||
const progressCallback: FileSystem.DownloadProgressCallback = (data) => {
|
// const progressCallback: FileSystem.DownloadProgressCallback = (data) => {
|
||||||
const { totalBytesWritten, totalBytesExpectedToWrite } = data;
|
// const { totalBytesWritten, totalBytesExpectedToWrite } = data;
|
||||||
const now = Date.now();
|
// const now = Date.now();
|
||||||
const last = lastBytesRef.current.get(id);
|
// const last = lastBytesRef.current.get(id);
|
||||||
let speedBps = 0;
|
// let speedBps = 0;
|
||||||
if (last) {
|
// if (last) {
|
||||||
const deltaBytes = totalBytesWritten - last.bytes;
|
// const deltaBytes = totalBytesWritten - last.bytes;
|
||||||
const deltaTime = Math.max(1, now - last.time) / 1000;
|
// const deltaTime = Math.max(1, now - last.time) / 1000;
|
||||||
speedBps = deltaBytes / deltaTime;
|
// speedBps = deltaBytes / deltaTime;
|
||||||
}
|
// }
|
||||||
lastBytesRef.current.set(id, { bytes: totalBytesWritten, time: now });
|
// lastBytesRef.current.set(id, { bytes: totalBytesWritten, time: now });
|
||||||
|
|
||||||
updateDownload(id, (d) => ({
|
// updateDownload(id, (d) => ({
|
||||||
...d,
|
// ...d,
|
||||||
downloadedBytes: totalBytesWritten,
|
// downloadedBytes: totalBytesWritten,
|
||||||
totalBytes: totalBytesExpectedToWrite || d.totalBytes,
|
// totalBytes: totalBytesExpectedToWrite || d.totalBytes,
|
||||||
progress: totalBytesExpectedToWrite ? Math.floor((totalBytesWritten / totalBytesExpectedToWrite) * 100) : d.progress,
|
// progress: totalBytesExpectedToWrite ? Math.floor((totalBytesWritten / totalBytesExpectedToWrite) * 100) : d.progress,
|
||||||
speedBps,
|
// speedBps,
|
||||||
status: 'downloading',
|
// status: 'downloading',
|
||||||
updatedAt: now,
|
// updatedAt: now,
|
||||||
}));
|
// }));
|
||||||
};
|
// };
|
||||||
|
|
||||||
let resumable = resumablesRef.current.get(id);
|
// let resumable = resumablesRef.current.get(id);
|
||||||
if (!resumable) {
|
// if (!resumable) {
|
||||||
resumable = FileSystem.createDownloadResumable(
|
// resumable = FileSystem.createDownloadResumable(
|
||||||
item.sourceUrl,
|
// item.sourceUrl,
|
||||||
item.fileUri || `${FileSystem.documentDirectory}downloads/${sanitizeFilename(item.title)}.mp4`,
|
// item.fileUri || `${FileSystem.documentDirectory}downloads/${sanitizeFilename(item.title)}.mp4`,
|
||||||
{ headers: item.headers || {} },
|
// { headers: item.headers || {} },
|
||||||
progressCallback
|
// progressCallback
|
||||||
);
|
// );
|
||||||
resumablesRef.current.set(id, resumable);
|
// resumablesRef.current.set(id, resumable);
|
||||||
}
|
// }
|
||||||
try {
|
// try {
|
||||||
const result = await resumable.resumeAsync();
|
// const result = await resumable.resumeAsync();
|
||||||
if (!result) throw new Error('Resume failed');
|
// if (!result) throw new Error('Resume failed');
|
||||||
updateDownload(id, (d) => ({ ...d, status: 'completed', progress: 100, updatedAt: Date.now(), fileUri: result.uri }));
|
// updateDownload(id, (d) => ({ ...d, status: 'completed', progress: 100, updatedAt: Date.now(), fileUri: result.uri }));
|
||||||
resumablesRef.current.delete(id);
|
// resumablesRef.current.delete(id);
|
||||||
lastBytesRef.current.delete(id);
|
// lastBytesRef.current.delete(id);
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
updateDownload(id, (d) => ({ ...d, status: 'error', updatedAt: Date.now() }));
|
// updateDownload(id, (d) => ({ ...d, status: 'error', updatedAt: Date.now() }));
|
||||||
resumablesRef.current.delete(id);
|
// resumablesRef.current.delete(id);
|
||||||
lastBytesRef.current.delete(id);
|
// lastBytesRef.current.delete(id);
|
||||||
}
|
// }
|
||||||
}, [downloads, updateDownload]);
|
// }, [downloads, updateDownload]);
|
||||||
|
|
||||||
const cancelDownload = useCallback(async (id: string) => {
|
const cancelDownload = useCallback(async (id: string) => {
|
||||||
const resumable = resumablesRef.current.get(id);
|
const resumable = resumablesRef.current.get(id);
|
||||||
|
|
@ -315,11 +356,11 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
const value = useMemo<DownloadsContextValue>(() => ({
|
const value = useMemo<DownloadsContextValue>(() => ({
|
||||||
downloads,
|
downloads,
|
||||||
startDownload,
|
startDownload,
|
||||||
pauseDownload,
|
// pauseDownload,
|
||||||
resumeDownload,
|
// resumeDownload,
|
||||||
cancelDownload,
|
cancelDownload,
|
||||||
removeDownload,
|
removeDownload,
|
||||||
}), [downloads, startDownload, pauseDownload, resumeDownload, cancelDownload, removeDownload]);
|
}), [downloads, startDownload, /*pauseDownload, resumeDownload,*/ cancelDownload, removeDownload]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DownloadsContext.Provider value={value}>
|
<DownloadsContext.Provider value={value}>
|
||||||
|
|
|
||||||
|
|
@ -100,11 +100,8 @@ const DownloadItemComponent: React.FC<{
|
||||||
const getStatusText = () => {
|
const getStatusText = () => {
|
||||||
switch (item.status) {
|
switch (item.status) {
|
||||||
case 'downloading':
|
case 'downloading':
|
||||||
{
|
const eta = item.etaSeconds ? `${Math.ceil(item.etaSeconds / 60)}m` : undefined;
|
||||||
const mbps = item.speedBps ? `${(item.speedBps / (1024 * 1024)).toFixed(2)} MB/s` : undefined;
|
return eta ? `Downloading • ${eta}` : 'Downloading';
|
||||||
const eta = item.etaSeconds ? `${Math.ceil(item.etaSeconds / 60)}m` : undefined;
|
|
||||||
return mbps ? `Downloading • ${mbps}${eta ? ` • ${eta}` : ''}` : 'Downloading';
|
|
||||||
}
|
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return 'Completed';
|
return 'Completed';
|
||||||
case 'paused':
|
case 'paused':
|
||||||
|
|
@ -121,12 +118,15 @@ const DownloadItemComponent: React.FC<{
|
||||||
const getActionIcon = () => {
|
const getActionIcon = () => {
|
||||||
switch (item.status) {
|
switch (item.status) {
|
||||||
case 'downloading':
|
case 'downloading':
|
||||||
return 'pause';
|
// return 'pause'; // Resume support commented out
|
||||||
|
return null;
|
||||||
case 'paused':
|
case 'paused':
|
||||||
case 'error':
|
case 'error':
|
||||||
return 'play';
|
// return 'play'; // Resume support commented out
|
||||||
|
return null;
|
||||||
case 'queued':
|
case 'queued':
|
||||||
return 'play';
|
// return 'play'; // Resume support commented out
|
||||||
|
return null;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -134,15 +134,15 @@ const DownloadItemComponent: React.FC<{
|
||||||
|
|
||||||
const handleActionPress = () => {
|
const handleActionPress = () => {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
|
||||||
switch (item.status) {
|
switch (item.status) {
|
||||||
case 'downloading':
|
case 'downloading':
|
||||||
onAction(item, 'pause');
|
// onAction(item, 'pause'); // Resume support commented out
|
||||||
break;
|
break;
|
||||||
case 'paused':
|
case 'paused':
|
||||||
case 'error':
|
case 'error':
|
||||||
case 'queued':
|
case 'queued':
|
||||||
onAction(item, 'resume');
|
// onAction(item, 'resume'); // Resume support commented out
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -172,15 +172,20 @@ const DownloadItemComponent: React.FC<{
|
||||||
{/* Progress section */}
|
{/* Progress section */}
|
||||||
<View style={styles.progressSection}>
|
<View style={styles.progressSection}>
|
||||||
{/* Provider + quality row */}
|
{/* Provider + quality row */}
|
||||||
<View style={styles.progressInfo}>
|
<View style={styles.providerRow}>
|
||||||
<Text style={[styles.statusText, { color: currentTheme.colors.mediumEmphasis }]}>
|
<Text style={[styles.providerText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
{(item.providerName || 'Provider') + (item.quality ? ` ${item.quality}` : '')}
|
{(item.providerName || 'Provider') + (item.quality ? ` ${item.quality}` : '')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.progressInfo}>
|
{/* Status row */}
|
||||||
|
<View style={styles.statusRow}>
|
||||||
<Text style={[styles.statusText, { color: getStatusColor() }]}>
|
<Text style={[styles.statusText, { color: getStatusColor() }]}>
|
||||||
{getStatusText()}
|
{getStatusText()}
|
||||||
</Text>
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Size row */}
|
||||||
|
<View style={styles.sizeRow}>
|
||||||
<Text style={[styles.progressText, { color: currentTheme.colors.mediumEmphasis }]}>
|
<Text style={[styles.progressText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
{formatBytes(item.downloadedBytes)} / {item.totalBytes ? formatBytes(item.totalBytes) : '—'}
|
{formatBytes(item.downloadedBytes)} / {item.totalBytes ? formatBytes(item.totalBytes) : '—'}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -257,10 +262,10 @@ const DownloadsScreen: React.FC = () => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const { top: safeAreaTop } = useSafeAreaInsets();
|
const { top: safeAreaTop } = useSafeAreaInsets();
|
||||||
const { downloads, pauseDownload, resumeDownload, cancelDownload } = useDownloads();
|
const { downloads, /*pauseDownload, resumeDownload,*/ cancelDownload } = useDownloads();
|
||||||
|
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [selectedFilter, setSelectedFilter] = useState<'all' | 'downloading' | 'completed' | 'paused'>('all');
|
const [selectedFilter, setSelectedFilter] = useState<'all' | 'downloading' | 'completed'>('all');
|
||||||
|
|
||||||
// Animation values
|
// Animation values
|
||||||
const headerOpacity = useSharedValue(1);
|
const headerOpacity = useSharedValue(1);
|
||||||
|
|
@ -274,8 +279,8 @@ const DownloadsScreen: React.FC = () => {
|
||||||
return item.status === 'downloading' || item.status === 'queued';
|
return item.status === 'downloading' || item.status === 'queued';
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return item.status === 'completed';
|
return item.status === 'completed';
|
||||||
case 'paused':
|
// case 'paused':
|
||||||
return item.status === 'paused' || item.status === 'error';
|
// return item.status === 'paused' || item.status === 'error'; // Resume support commented out
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -285,15 +290,15 @@ const DownloadsScreen: React.FC = () => {
|
||||||
// Statistics
|
// Statistics
|
||||||
const stats = useMemo(() => {
|
const stats = useMemo(() => {
|
||||||
const total = downloads.length;
|
const total = downloads.length;
|
||||||
const downloading = downloads.filter(item =>
|
const downloading = downloads.filter(item =>
|
||||||
item.status === 'downloading' || item.status === 'queued'
|
item.status === 'downloading' || item.status === 'queued'
|
||||||
).length;
|
).length;
|
||||||
const completed = downloads.filter(item => item.status === 'completed').length;
|
const completed = downloads.filter(item => item.status === 'completed').length;
|
||||||
const paused = downloads.filter(item =>
|
// const paused = downloads.filter(item =>
|
||||||
item.status === 'paused' || item.status === 'error'
|
// item.status === 'paused' || item.status === 'error'
|
||||||
).length;
|
// ).length; // Resume support commented out
|
||||||
|
|
||||||
return { total, downloading, completed, paused };
|
return { total, downloading, completed /*, paused*/ };
|
||||||
}, [downloads]);
|
}, [downloads]);
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
|
|
@ -345,12 +350,12 @@ const DownloadsScreen: React.FC = () => {
|
||||||
}, [navigation]);
|
}, [navigation]);
|
||||||
|
|
||||||
const handleDownloadAction = useCallback((item: DownloadItem, action: 'pause' | 'resume' | 'cancel' | 'retry') => {
|
const handleDownloadAction = useCallback((item: DownloadItem, action: 'pause' | 'resume' | 'cancel' | 'retry') => {
|
||||||
if (action === 'pause') pauseDownload(item.id);
|
// if (action === 'pause') pauseDownload(item.id);
|
||||||
if (action === 'resume') resumeDownload(item.id);
|
// if (action === 'resume') resumeDownload(item.id);
|
||||||
if (action === 'cancel') cancelDownload(item.id);
|
if (action === 'cancel') cancelDownload(item.id);
|
||||||
}, [pauseDownload, resumeDownload, cancelDownload]);
|
}, [/*pauseDownload, resumeDownload,*/ cancelDownload]);
|
||||||
|
|
||||||
const handleFilterPress = useCallback((filter: typeof selectedFilter) => {
|
const handleFilterPress = useCallback((filter: 'all' | 'downloading' | 'completed') => {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
setSelectedFilter(filter);
|
setSelectedFilter(filter);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -442,7 +447,7 @@ const DownloadsScreen: React.FC = () => {
|
||||||
{renderFilterButton('all', 'All', stats.total)}
|
{renderFilterButton('all', 'All', stats.total)}
|
||||||
{renderFilterButton('downloading', 'Active', stats.downloading)}
|
{renderFilterButton('downloading', 'Active', stats.downloading)}
|
||||||
{renderFilterButton('completed', 'Done', stats.completed)}
|
{renderFilterButton('completed', 'Done', stats.completed)}
|
||||||
{renderFilterButton('paused', 'Paused', stats.paused)}
|
{/* {renderFilterButton('paused', 'Paused', stats.paused)} */} {/* Resume support commented out */}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
@ -583,15 +588,30 @@ const styles = StyleSheet.create({
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
progressSection: {
|
progressSection: {
|
||||||
gap: 8,
|
gap: 4,
|
||||||
|
},
|
||||||
|
providerRow: {
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
providerText: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
statusRow: {
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
sizeRow: {
|
||||||
|
marginBottom: 6,
|
||||||
},
|
},
|
||||||
progressInfo: {
|
progressInfo: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 8,
|
||||||
},
|
},
|
||||||
statusText: {
|
statusText: {
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
progressText: {
|
progressText: {
|
||||||
|
|
|
||||||
|
|
@ -303,7 +303,8 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
|
||||||
const season = typeof parentSeason === 'number' ? parentSeason : (parent.season || parent.season_number);
|
const season = typeof parentSeason === 'number' ? parentSeason : (parent.season || parent.season_number);
|
||||||
const episode = typeof parentEpisode === 'number' ? parentEpisode : (parent.episode || parent.episode_number);
|
const episode = typeof parentEpisode === 'number' ? parentEpisode : (parent.episode || parent.episode_number);
|
||||||
const episodeTitle = parentEpisodeTitle || parent.episodeTitle || parent.episode_name;
|
const episodeTitle = parentEpisodeTitle || parent.episodeTitle || parent.episode_name;
|
||||||
const provider = providerName || parent.addonName || parent.addonId || (stream.addonName as any) || (stream.addonId as any) || 'Provider';
|
// 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;
|
const idForContent = parent.imdbId || parent.tmdbId || parent.addonId || inferredTitle;
|
||||||
|
|
||||||
await startDownload({
|
await startDownload({
|
||||||
|
|
|
||||||
|
|
@ -320,6 +320,45 @@ class NotificationService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Immediate notifications for download progress (background only)
|
||||||
|
public async notifyDownloadProgress(title: string, progress: number, downloadedBytes?: number, totalBytes?: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!this.settings.enabled) return;
|
||||||
|
if (AppState.currentState === 'active') return;
|
||||||
|
const downloadedMb = Math.floor((downloadedBytes || 0) / (1024 * 1024));
|
||||||
|
const totalMb = totalBytes ? Math.floor(totalBytes / (1024 * 1024)) : undefined;
|
||||||
|
const body = `${progress}%` + (totalMb !== undefined ? ` • ${downloadedMb}MB / ${totalMb}MB` : '');
|
||||||
|
await Notifications.scheduleNotificationAsync({
|
||||||
|
content: {
|
||||||
|
title: `Downloading ${title}`,
|
||||||
|
body,
|
||||||
|
data: { kind: 'download-progress' },
|
||||||
|
},
|
||||||
|
trigger: null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[NotificationService] notifyDownloadProgress error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Immediate notification for download completion (background only)
|
||||||
|
public async notifyDownloadComplete(title: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!this.settings.enabled) return;
|
||||||
|
if (AppState.currentState === 'active') return;
|
||||||
|
await Notifications.scheduleNotificationAsync({
|
||||||
|
content: {
|
||||||
|
title: 'Download complete',
|
||||||
|
body: title,
|
||||||
|
data: { kind: 'download-complete' },
|
||||||
|
},
|
||||||
|
trigger: null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[NotificationService] notifyDownloadComplete error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sync notifications for all library items using memory-efficient batching
|
// Sync notifications for all library items using memory-efficient batching
|
||||||
private async syncNotificationsForLibrary(libraryItems: any[]): Promise<void> {
|
private async syncNotificationsForLibrary(libraryItems: any[]): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue