added external player selection for downloads

This commit is contained in:
tapframe 2025-12-11 15:00:15 +05:30
parent e160bf6fe0
commit 01953af578
3 changed files with 223 additions and 76 deletions

View file

@ -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');

View file

@ -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); }}
/> />

View file

@ -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>