mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +00:00
added external player selection for downloads
This commit is contained in:
parent
e160bf6fe0
commit
01953af578
3 changed files with 223 additions and 76 deletions
|
|
@ -87,6 +87,7 @@ export interface AppSettings {
|
||||||
openMetadataScreenWhenCacheDisabled: boolean; // When cache disabled, open MetadataScreen instead of StreamsScreen
|
openMetadataScreenWhenCacheDisabled: boolean; // When cache disabled, open MetadataScreen instead of StreamsScreen
|
||||||
streamCacheTTL: number; // Stream cache duration in milliseconds (default: 1 hour)
|
streamCacheTTL: number; // Stream cache duration in milliseconds (default: 1 hour)
|
||||||
enableStreamsBackdrop: boolean; // Enable blurred backdrop background on StreamsScreen mobile
|
enableStreamsBackdrop: boolean; // Enable blurred backdrop background on StreamsScreen mobile
|
||||||
|
useExternalPlayerForDownloads: boolean; // Enable/disable external player for downloaded content
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: AppSettings = {
|
export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
|
|
@ -122,6 +123,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
alwaysResume: true,
|
alwaysResume: true,
|
||||||
// Downloads
|
// Downloads
|
||||||
enableDownloads: false,
|
enableDownloads: false,
|
||||||
|
useExternalPlayerForDownloads: false,
|
||||||
// Theme defaults
|
// Theme defaults
|
||||||
themeId: 'default',
|
themeId: 'default',
|
||||||
customThemes: [],
|
customThemes: [],
|
||||||
|
|
@ -162,12 +164,12 @@ export const useSettings = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
|
||||||
// Subscribe to settings changes
|
// Subscribe to settings changes
|
||||||
const unsubscribe = settingsEmitter.addListener(() => {
|
const unsubscribe = settingsEmitter.addListener(() => {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -183,13 +185,13 @@ export const useSettings = () => {
|
||||||
|
|
||||||
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
||||||
const scopedKey = `@user:${scope}:${SETTINGS_STORAGE_KEY}`;
|
const scopedKey = `@user:${scope}:${SETTINGS_STORAGE_KEY}`;
|
||||||
|
|
||||||
// Use synchronous MMKV reads for better performance
|
// Use synchronous MMKV reads for better performance
|
||||||
const [scopedJson, legacyJson] = await Promise.all([
|
const [scopedJson, legacyJson] = await Promise.all([
|
||||||
mmkvStorage.getItem(scopedKey),
|
mmkvStorage.getItem(scopedKey),
|
||||||
mmkvStorage.getItem(SETTINGS_STORAGE_KEY),
|
mmkvStorage.getItem(SETTINGS_STORAGE_KEY),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const parsedScoped = scopedJson ? JSON.parse(scopedJson) : null;
|
const parsedScoped = scopedJson ? JSON.parse(scopedJson) : null;
|
||||||
const parsedLegacy = legacyJson ? JSON.parse(legacyJson) : null;
|
const parsedLegacy = legacyJson ? JSON.parse(legacyJson) : null;
|
||||||
|
|
||||||
|
|
@ -202,16 +204,16 @@ export const useSettings = () => {
|
||||||
if (scoped) {
|
if (scoped) {
|
||||||
try {
|
try {
|
||||||
merged = JSON.parse(scoped);
|
merged = JSON.parse(scoped);
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalSettings = merged ? { ...DEFAULT_SETTINGS, ...merged } : DEFAULT_SETTINGS;
|
const finalSettings = merged ? { ...DEFAULT_SETTINGS, ...merged } : DEFAULT_SETTINGS;
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
cachedSettings = finalSettings;
|
cachedSettings = finalSettings;
|
||||||
settingsCacheTimestamp = now;
|
settingsCacheTimestamp = now;
|
||||||
|
|
||||||
setSettings(finalSettings);
|
setSettings(finalSettings);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (__DEV__) console.error('Failed to load settings:', error);
|
if (__DEV__) console.error('Failed to load settings:', error);
|
||||||
|
|
@ -231,23 +233,23 @@ export const useSettings = () => {
|
||||||
) => {
|
) => {
|
||||||
const newSettings = { ...settings, [key]: value };
|
const newSettings = { ...settings, [key]: value };
|
||||||
try {
|
try {
|
||||||
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
||||||
const scopedKey = `@user:${scope}:${SETTINGS_STORAGE_KEY}`;
|
const scopedKey = `@user:${scope}:${SETTINGS_STORAGE_KEY}`;
|
||||||
// Write to both scoped key (multi-user aware) and legacy key for backward compatibility
|
// Write to both scoped key (multi-user aware) and legacy key for backward compatibility
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
mmkvStorage.setItem(scopedKey, JSON.stringify(newSettings)),
|
mmkvStorage.setItem(scopedKey, JSON.stringify(newSettings)),
|
||||||
mmkvStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(newSettings)),
|
mmkvStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(newSettings)),
|
||||||
]);
|
]);
|
||||||
// Ensure a current scope exists to avoid future loads missing the chosen scope
|
// Ensure a current scope exists to avoid future loads missing the chosen scope
|
||||||
await mmkvStorage.setItem('@user:current', scope);
|
await mmkvStorage.setItem('@user:current', scope);
|
||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
cachedSettings = newSettings;
|
cachedSettings = newSettings;
|
||||||
settingsCacheTimestamp = Date.now();
|
settingsCacheTimestamp = Date.now();
|
||||||
|
|
||||||
setSettings(newSettings);
|
setSettings(newSettings);
|
||||||
if (__DEV__) console.log(`Setting updated: ${key}`, value);
|
if (__DEV__) console.log(`Setting updated: ${key}`, value);
|
||||||
|
|
||||||
// Notify all subscribers that settings have changed (if requested)
|
// Notify all subscribers that settings have changed (if requested)
|
||||||
if (emitEvent) {
|
if (emitEvent) {
|
||||||
if (__DEV__) console.log('Emitting settings change event');
|
if (__DEV__) console.log('Emitting settings change event');
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
Alert,
|
Alert,
|
||||||
Platform,
|
Platform,
|
||||||
Clipboard,
|
Clipboard,
|
||||||
|
Linking,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||||
|
|
@ -28,6 +29,8 @@ import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import FastImage from '@d11/react-native-fast-image';
|
import FastImage from '@d11/react-native-fast-image';
|
||||||
import { useDownloads } from '../contexts/DownloadsContext';
|
import { useDownloads } from '../contexts/DownloadsContext';
|
||||||
|
import { useSettings } from '../hooks/useSettings';
|
||||||
|
import { VideoPlayerService } from '../services/videoPlayerService';
|
||||||
import type { DownloadItem } from '../contexts/DownloadsContext';
|
import type { DownloadItem } from '../contexts/DownloadsContext';
|
||||||
import { useToast } from '../contexts/ToastContext';
|
import { useToast } from '../contexts/ToastContext';
|
||||||
import CustomAlert from '../components/CustomAlert';
|
import CustomAlert from '../components/CustomAlert';
|
||||||
|
|
@ -60,7 +63,7 @@ const optimizePosterUrl = (poster: string | undefined | null): string => {
|
||||||
// Empty state component
|
// Empty state component
|
||||||
const EmptyDownloadsState: React.FC<{ navigation: NavigationProp<RootStackParamList> }> = ({ navigation }) => {
|
const EmptyDownloadsState: React.FC<{ navigation: NavigationProp<RootStackParamList> }> = ({ navigation }) => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.emptyContainer}>
|
<View style={styles.emptyContainer}>
|
||||||
<View style={[styles.emptyIconContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
<View style={[styles.emptyIconContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||||
|
|
@ -76,7 +79,7 @@ const EmptyDownloadsState: React.FC<{ navigation: NavigationProp<RootStackParamL
|
||||||
<Text style={[styles.emptySubtitle, { color: currentTheme.colors.mediumEmphasis }]}>
|
<Text style={[styles.emptySubtitle, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
Downloaded content will appear here for offline viewing
|
Downloaded content will appear here for offline viewing
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.exploreButton, { backgroundColor: currentTheme.colors.primary }]}
|
style={[styles.exploreButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigation.navigate('Search');
|
navigation.navigate('Search');
|
||||||
|
|
@ -129,12 +132,12 @@ const DownloadItemComponent: React.FC<{
|
||||||
|
|
||||||
const formatBytes = (bytes?: number) => {
|
const formatBytes = (bytes?: number) => {
|
||||||
if (!bytes || bytes <= 0) return '0 B';
|
if (!bytes || bytes <= 0) return '0 B';
|
||||||
const sizes = ['B','KB','MB','GB','TB'];
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
const v = bytes / Math.pow(1024, i);
|
const v = bytes / Math.pow(1024, i);
|
||||||
return `${v.toFixed(v >= 100 ? 0 : v >= 10 ? 1 : 2)} ${sizes[i]}`;
|
return `${v.toFixed(v >= 100 ? 0 : v >= 10 ? 1 : 2)} ${sizes[i]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusColor = () => {
|
const getStatusColor = () => {
|
||||||
switch (item.status) {
|
switch (item.status) {
|
||||||
case 'downloading':
|
case 'downloading':
|
||||||
|
|
@ -218,10 +221,10 @@ const DownloadItemComponent: React.FC<{
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name={
|
name={
|
||||||
item.status === 'completed' ? 'check' :
|
item.status === 'completed' ? 'check' :
|
||||||
item.status === 'downloading' ? 'download' :
|
item.status === 'downloading' ? 'download' :
|
||||||
item.status === 'paused' ? 'pause' :
|
item.status === 'paused' ? 'pause' :
|
||||||
item.status === 'error' ? 'alert-circle' :
|
item.status === 'error' ? 'alert-circle' :
|
||||||
'clock'
|
'clock'
|
||||||
}
|
}
|
||||||
size={12}
|
size={12}
|
||||||
color="white"
|
color="white"
|
||||||
|
|
@ -234,10 +237,10 @@ const DownloadItemComponent: React.FC<{
|
||||||
<View style={styles.downloadHeader}>
|
<View style={styles.downloadHeader}>
|
||||||
<View style={styles.titleContainer}>
|
<View style={styles.titleContainer}>
|
||||||
<Text style={[styles.downloadTitle, { color: currentTheme.colors.text }]} numberOfLines={1}>
|
<Text style={[styles.downloadTitle, { color: currentTheme.colors.text }]} numberOfLines={1}>
|
||||||
{item.title}{item.type === 'series' && item.season && item.episode ? ` S${String(item.season).padStart(2,'0')}E${String(item.episode).padStart(2,'0')}` : ''}
|
{item.title}{item.type === 'series' && item.season && item.episode ? ` S${String(item.season).padStart(2, '0')}E${String(item.episode).padStart(2, '0')}` : ''}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{item.type === 'series' && (
|
{item.type === 'series' && (
|
||||||
<Text style={[styles.episodeInfo, { color: currentTheme.colors.mediumEmphasis }]} numberOfLines={1}>
|
<Text style={[styles.episodeInfo, { color: currentTheme.colors.mediumEmphasis }]} numberOfLines={1}>
|
||||||
S{item.season?.toString().padStart(2, '0')}E{item.episode?.toString().padStart(2, '0')} • {item.episodeTitle}
|
S{item.season?.toString().padStart(2, '0')}E{item.episode?.toString().padStart(2, '0')} • {item.episodeTitle}
|
||||||
|
|
@ -293,7 +296,7 @@ const DownloadItemComponent: React.FC<{
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.progressDetails}>
|
<View style={styles.progressDetails}>
|
||||||
<Text style={[styles.progressPercentage, { color: currentTheme.colors.text }]}>
|
<Text style={[styles.progressPercentage, { color: currentTheme.colors.text }]}>
|
||||||
{item.progress || 0}%
|
{item.progress || 0}%
|
||||||
|
|
@ -322,7 +325,7 @@ const DownloadItemComponent: React.FC<{
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.actionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
|
style={[styles.actionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
|
||||||
onPress={() => onRequestRemove(item)}
|
onPress={() => onRequestRemove(item)}
|
||||||
|
|
@ -342,6 +345,7 @@ const DownloadItemComponent: React.FC<{
|
||||||
const DownloadsScreen: React.FC = () => {
|
const DownloadsScreen: React.FC = () => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
const { settings } = useSettings();
|
||||||
const { top: safeAreaTop } = useSafeAreaInsets();
|
const { top: safeAreaTop } = useSafeAreaInsets();
|
||||||
const { downloads, pauseDownload, resumeDownload, cancelDownload } = useDownloads();
|
const { downloads, pauseDownload, resumeDownload, cancelDownload } = useDownloads();
|
||||||
const { showSuccess, showInfo } = useToast();
|
const { showSuccess, showInfo } = useToast();
|
||||||
|
|
@ -394,7 +398,7 @@ const DownloadsScreen: React.FC = () => {
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDownloadPress = useCallback((item: DownloadItem) => {
|
const handleDownloadPress = useCallback(async (item: DownloadItem) => {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
if (item.status !== 'completed') {
|
if (item.status !== 'completed') {
|
||||||
Alert.alert('Download not ready', 'Please wait until the download completes.');
|
Alert.alert('Download not ready', 'Please wait until the download completes.');
|
||||||
|
|
@ -411,33 +415,132 @@ const DownloadsScreen: React.FC = () => {
|
||||||
const isMp4 = /\.mp4(\?|$)/i.test(lower);
|
const isMp4 = /\.mp4(\?|$)/i.test(lower);
|
||||||
const videoType = isM3u8 ? 'm3u8' : isMpd ? 'mpd' : isMp4 ? 'mp4' : undefined;
|
const videoType = isM3u8 ? 'm3u8' : isMpd ? 'mpd' : isMp4 ? 'mp4' : undefined;
|
||||||
|
|
||||||
// Build episodeId for series progress tracking (format: contentId:season:episode)
|
// Use external player if enabled in settings
|
||||||
const episodeId = item.type === 'series' && item.season && item.episode
|
if (settings.useExternalPlayerForDownloads) {
|
||||||
? `${item.contentId}:${item.season}:${item.episode}`
|
if (Platform.OS === 'android') {
|
||||||
: undefined;
|
try {
|
||||||
|
// Use VideoPlayerService for Android external playback
|
||||||
|
const success = await VideoPlayerService.playVideo(uri, {
|
||||||
|
useExternalPlayer: true,
|
||||||
|
title: item.title,
|
||||||
|
episodeTitle: item.type === 'series' ? item.episodeTitle : undefined,
|
||||||
|
episodeNumber: item.type === 'series' && item.season && item.episode ? `S${item.season}E${item.episode}` : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const playerRoute = Platform.OS === 'ios' ? 'PlayerIOS' : 'PlayerAndroid';
|
if (success) return;
|
||||||
navigation.navigate(playerRoute as any, {
|
// Fall through to internal player if external fails
|
||||||
uri,
|
} catch (error) {
|
||||||
title: item.title,
|
console.error('External player failed:', error);
|
||||||
episodeTitle: item.type === 'series' ? item.episodeTitle : undefined,
|
// Fall through to internal player
|
||||||
season: item.type === 'series' ? item.season : undefined,
|
}
|
||||||
episode: item.type === 'series' ? item.episode : undefined,
|
} else if (Platform.OS === 'ios') {
|
||||||
quality: item.quality,
|
const streamUrl = encodeURIComponent(uri);
|
||||||
year: undefined,
|
let externalPlayerUrls: string[] = [];
|
||||||
streamProvider: 'Downloads',
|
|
||||||
streamName: item.providerName || 'Offline',
|
switch (settings.preferredPlayer) {
|
||||||
headers: undefined,
|
case 'vlc':
|
||||||
forceVlc: Platform.OS === 'android' ? isMkv : false,
|
externalPlayerUrls = [
|
||||||
id: item.contentId, // Use contentId (base ID) instead of compound id for progress tracking
|
`vlc://${uri}`,
|
||||||
type: item.type,
|
`vlc-x-callback://x-callback-url/stream?url=${streamUrl}`,
|
||||||
episodeId: episodeId, // Pass episodeId for series progress tracking
|
`vlc://${streamUrl}`
|
||||||
imdbId: (item as any).imdbId || item.contentId, // Use imdbId if available, fallback to contentId
|
];
|
||||||
availableStreams: {},
|
break;
|
||||||
backdrop: undefined,
|
|
||||||
videoType,
|
case 'outplayer':
|
||||||
} as any);
|
externalPlayerUrls = [
|
||||||
}, [navigation]);
|
`outplayer://${uri}`,
|
||||||
|
`outplayer://${streamUrl}`,
|
||||||
|
`outplayer://play?url=${streamUrl}`,
|
||||||
|
`outplayer://stream?url=${streamUrl}`,
|
||||||
|
`outplayer://play/browser?url=${streamUrl}`
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'infuse':
|
||||||
|
externalPlayerUrls = [
|
||||||
|
`infuse://x-callback-url/play?url=${streamUrl}`,
|
||||||
|
`infuse://play?url=${streamUrl}`,
|
||||||
|
`infuse://${streamUrl}`
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'vidhub':
|
||||||
|
externalPlayerUrls = [
|
||||||
|
`vidhub://play?url=${streamUrl}`,
|
||||||
|
`vidhub://${streamUrl}`
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'infuse_livecontainer':
|
||||||
|
const infuseUrls = [
|
||||||
|
`infuse://x-callback-url/play?url=${streamUrl}`,
|
||||||
|
`infuse://play?url=${streamUrl}`,
|
||||||
|
`infuse://${streamUrl}`
|
||||||
|
];
|
||||||
|
externalPlayerUrls = infuseUrls.map(infuseUrl => {
|
||||||
|
const encoded = Buffer.from(infuseUrl).toString('base64');
|
||||||
|
return `livecontainer://open-url?url=${encoded}`;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Internal logic will handle 'internal' choice
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.preferredPlayer !== 'internal') {
|
||||||
|
// Try each URL format in sequence
|
||||||
|
const tryNextUrl = (index: number) => {
|
||||||
|
if (index >= externalPlayerUrls.length) {
|
||||||
|
// Fallback to internal player if all external attempts fail
|
||||||
|
openInternalPlayer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = externalPlayerUrls[index];
|
||||||
|
Linking.openURL(url)
|
||||||
|
.catch(() => tryNextUrl(index + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (externalPlayerUrls.length > 0) {
|
||||||
|
tryNextUrl(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openInternalPlayer = () => {
|
||||||
|
// 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,
|
||||||
|
title: item.title,
|
||||||
|
episodeTitle: item.type === 'series' ? item.episodeTitle : undefined,
|
||||||
|
season: item.type === 'series' ? item.season : undefined,
|
||||||
|
episode: item.type === 'series' ? item.episode : undefined,
|
||||||
|
quality: item.quality,
|
||||||
|
year: undefined,
|
||||||
|
streamProvider: 'Downloads',
|
||||||
|
streamName: item.providerName || 'Offline',
|
||||||
|
headers: undefined,
|
||||||
|
forceVlc: Platform.OS === 'android' ? isMkv : false,
|
||||||
|
id: item.contentId, // Use contentId (base ID) instead of compound id for progress tracking
|
||||||
|
type: item.type,
|
||||||
|
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,
|
||||||
|
} as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
openInternalPlayer();
|
||||||
|
}, [navigation, settings]);
|
||||||
|
|
||||||
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);
|
||||||
|
|
@ -479,8 +582,8 @@ const DownloadsScreen: React.FC = () => {
|
||||||
style={[
|
style={[
|
||||||
styles.filterButton,
|
styles.filterButton,
|
||||||
{
|
{
|
||||||
backgroundColor: selectedFilter === filter
|
backgroundColor: selectedFilter === filter
|
||||||
? currentTheme.colors.primary
|
? currentTheme.colors.primary
|
||||||
: currentTheme.colors.elevation1,
|
: currentTheme.colors.elevation1,
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
|
@ -490,8 +593,8 @@ const DownloadsScreen: React.FC = () => {
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.filterButtonText,
|
styles.filterButtonText,
|
||||||
{
|
{
|
||||||
color: selectedFilter === filter
|
color: selectedFilter === filter
|
||||||
? currentTheme.colors.white
|
? currentTheme.colors.white
|
||||||
: currentTheme.colors.text,
|
: currentTheme.colors.text,
|
||||||
}
|
}
|
||||||
]}>
|
]}>
|
||||||
|
|
@ -501,16 +604,16 @@ const DownloadsScreen: React.FC = () => {
|
||||||
<View style={[
|
<View style={[
|
||||||
styles.filterBadge,
|
styles.filterBadge,
|
||||||
{
|
{
|
||||||
backgroundColor: selectedFilter === filter
|
backgroundColor: selectedFilter === filter
|
||||||
? currentTheme.colors.white
|
? currentTheme.colors.white
|
||||||
: currentTheme.colors.primary,
|
: currentTheme.colors.primary,
|
||||||
}
|
}
|
||||||
]}>
|
]}>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.filterBadgeText,
|
styles.filterBadgeText,
|
||||||
{
|
{
|
||||||
color: selectedFilter === filter
|
color: selectedFilter === filter
|
||||||
? currentTheme.colors.primary
|
? currentTheme.colors.primary
|
||||||
: currentTheme.colors.white,
|
: currentTheme.colors.white,
|
||||||
}
|
}
|
||||||
]}>
|
]}>
|
||||||
|
|
@ -534,8 +637,8 @@ const DownloadsScreen: React.FC = () => {
|
||||||
styles.header,
|
styles.header,
|
||||||
{
|
{
|
||||||
backgroundColor: currentTheme.colors.darkBackground,
|
backgroundColor: currentTheme.colors.darkBackground,
|
||||||
paddingTop: (Platform.OS === 'android'
|
paddingTop: (Platform.OS === 'android'
|
||||||
? (StatusBar.currentHeight || 0) + 26
|
? (StatusBar.currentHeight || 0) + 26
|
||||||
: safeAreaTop + 15) + (isTablet ? 64 : 0),
|
: safeAreaTop + 15) + (isTablet ? 64 : 0),
|
||||||
borderBottomColor: currentTheme.colors.border,
|
borderBottomColor: currentTheme.colors.border,
|
||||||
},
|
},
|
||||||
|
|
@ -557,7 +660,7 @@ const DownloadsScreen: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{downloads.length > 0 && (
|
{downloads.length > 0 && (
|
||||||
<View style={styles.filterContainer}>
|
<View style={styles.filterContainer}>
|
||||||
{renderFilterButton('all', 'All', stats.total)}
|
{renderFilterButton('all', 'All', stats.total)}
|
||||||
|
|
@ -624,10 +727,10 @@ const DownloadsScreen: React.FC = () => {
|
||||||
<CustomAlert
|
<CustomAlert
|
||||||
visible={showRemoveAlert}
|
visible={showRemoveAlert}
|
||||||
title="Remove Download"
|
title="Remove Download"
|
||||||
message={pendingRemoveItem ? `Remove \"${pendingRemoveItem.title}\"${pendingRemoveItem.type === 'series' && pendingRemoveItem.season && pendingRemoveItem.episode ? ` S${String(pendingRemoveItem.season).padStart(2,'0')}E${String(pendingRemoveItem.episode).padStart(2,'0')}` : ''}?` : 'Remove this download?'}
|
message={pendingRemoveItem ? `Remove \"${pendingRemoveItem.title}\"${pendingRemoveItem.type === 'series' && pendingRemoveItem.season && pendingRemoveItem.episode ? ` S${String(pendingRemoveItem.season).padStart(2, '0')}E${String(pendingRemoveItem.episode).padStart(2, '0')}` : ''}?` : 'Remove this download?'}
|
||||||
actions={[
|
actions={[
|
||||||
{ label: 'Cancel', onPress: () => setShowRemoveAlert(false) },
|
{ label: 'Cancel', onPress: () => setShowRemoveAlert(false) },
|
||||||
{ label: 'Remove', onPress: () => { if (pendingRemoveItem) { cancelDownload(pendingRemoveItem.id); } setShowRemoveAlert(false); setPendingRemoveItem(null); }, style: { } },
|
{ label: 'Remove', onPress: () => { if (pendingRemoveItem) { cancelDownload(pendingRemoveItem.id); } setShowRemoveAlert(false); setPendingRemoveItem(null); }, style: {} },
|
||||||
]}
|
]}
|
||||||
onClose={() => { setShowRemoveAlert(false); setPendingRemoveItem(null); }}
|
onClose={() => { setShowRemoveAlert(false); setPendingRemoveItem(null); }}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
||||||
isLast,
|
isLast,
|
||||||
}) => {
|
}) => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
|
|
@ -175,17 +175,17 @@ const PlayerSettingsScreen: React.FC = () => {
|
||||||
Settings
|
Settings
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.headerActions}>
|
<View style={styles.headerActions}>
|
||||||
{/* Empty for now, but ready for future actions */}
|
{/* Empty for now, but ready for future actions */}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
||||||
Video Player
|
Video Player
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.scrollView}
|
style={styles.scrollView}
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
>
|
>
|
||||||
|
|
@ -229,7 +229,7 @@ const PlayerSettingsScreen: React.FC = () => {
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
|
|
@ -322,6 +322,48 @@ const PlayerSettingsScreen: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* External Player for Downloads */}
|
||||||
|
{((Platform.OS === 'android' && settings.useExternalPlayer) ||
|
||||||
|
(Platform.OS === 'ios' && settings.preferredPlayer !== 'internal')) && (
|
||||||
|
<View style={[styles.settingItem, styles.settingItemBorder, { borderBottomWidth: 0, borderTopWidth: 1, borderTopColor: 'rgba(255,255,255,0.08)' }]}>
|
||||||
|
<View style={styles.settingContent}>
|
||||||
|
<View style={[
|
||||||
|
styles.settingIconContainer,
|
||||||
|
{ backgroundColor: 'rgba(255,255,255,0.1)' }
|
||||||
|
]}>
|
||||||
|
<MaterialIcons
|
||||||
|
name="open-in-new"
|
||||||
|
size={20}
|
||||||
|
color={currentTheme.colors.primary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={styles.settingText}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.settingTitle,
|
||||||
|
{ color: currentTheme.colors.text },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
External Player for Downloads
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.settingDescription,
|
||||||
|
{ color: currentTheme.colors.textMuted },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Play downloaded content in your preferred external player.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={settings.useExternalPlayerForDownloads}
|
||||||
|
onValueChange={(value) => updateSetting('useExternalPlayerForDownloads', value)}
|
||||||
|
thumbColor={settings.useExternalPlayerForDownloads ? currentTheme.colors.primary : undefined}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue