mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-29 13:59:58 +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
|
||||
streamCacheTTL: number; // Stream cache duration in milliseconds (default: 1 hour)
|
||||
enableStreamsBackdrop: boolean; // Enable blurred backdrop background on StreamsScreen mobile
|
||||
useExternalPlayerForDownloads: boolean; // Enable/disable external player for downloaded content
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: AppSettings = {
|
||||
|
|
@ -122,6 +123,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||
alwaysResume: true,
|
||||
// Downloads
|
||||
enableDownloads: false,
|
||||
useExternalPlayerForDownloads: false,
|
||||
// Theme defaults
|
||||
themeId: 'default',
|
||||
customThemes: [],
|
||||
|
|
@ -162,12 +164,12 @@ export const useSettings = () => {
|
|||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
|
||||
|
||||
// Subscribe to settings changes
|
||||
const unsubscribe = settingsEmitter.addListener(() => {
|
||||
loadSettings();
|
||||
});
|
||||
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
|
|
@ -183,13 +185,13 @@ export const useSettings = () => {
|
|||
|
||||
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
||||
const scopedKey = `@user:${scope}:${SETTINGS_STORAGE_KEY}`;
|
||||
|
||||
|
||||
// Use synchronous MMKV reads for better performance
|
||||
const [scopedJson, legacyJson] = await Promise.all([
|
||||
mmkvStorage.getItem(scopedKey),
|
||||
mmkvStorage.getItem(SETTINGS_STORAGE_KEY),
|
||||
]);
|
||||
|
||||
|
||||
const parsedScoped = scopedJson ? JSON.parse(scopedJson) : null;
|
||||
const parsedLegacy = legacyJson ? JSON.parse(legacyJson) : null;
|
||||
|
||||
|
|
@ -202,16 +204,16 @@ export const useSettings = () => {
|
|||
if (scoped) {
|
||||
try {
|
||||
merged = JSON.parse(scoped);
|
||||
} catch {}
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
const finalSettings = merged ? { ...DEFAULT_SETTINGS, ...merged } : DEFAULT_SETTINGS;
|
||||
|
||||
|
||||
// Update cache
|
||||
cachedSettings = finalSettings;
|
||||
settingsCacheTimestamp = now;
|
||||
|
||||
|
||||
setSettings(finalSettings);
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error('Failed to load settings:', error);
|
||||
|
|
@ -231,23 +233,23 @@ export const useSettings = () => {
|
|||
) => {
|
||||
const newSettings = { ...settings, [key]: value };
|
||||
try {
|
||||
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
||||
const scopedKey = `@user:${scope}:${SETTINGS_STORAGE_KEY}`;
|
||||
// Write to both scoped key (multi-user aware) and legacy key for backward compatibility
|
||||
await Promise.all([
|
||||
mmkvStorage.setItem(scopedKey, JSON.stringify(newSettings)),
|
||||
mmkvStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(newSettings)),
|
||||
]);
|
||||
// Ensure a current scope exists to avoid future loads missing the chosen scope
|
||||
await mmkvStorage.setItem('@user:current', scope);
|
||||
|
||||
const scope = (await mmkvStorage.getItem('@user:current')) || 'local';
|
||||
const scopedKey = `@user:${scope}:${SETTINGS_STORAGE_KEY}`;
|
||||
// Write to both scoped key (multi-user aware) and legacy key for backward compatibility
|
||||
await Promise.all([
|
||||
mmkvStorage.setItem(scopedKey, JSON.stringify(newSettings)),
|
||||
mmkvStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(newSettings)),
|
||||
]);
|
||||
// Ensure a current scope exists to avoid future loads missing the chosen scope
|
||||
await mmkvStorage.setItem('@user:current', scope);
|
||||
|
||||
// Update cache
|
||||
cachedSettings = newSettings;
|
||||
settingsCacheTimestamp = Date.now();
|
||||
|
||||
|
||||
setSettings(newSettings);
|
||||
if (__DEV__) console.log(`Setting updated: ${key}`, value);
|
||||
|
||||
|
||||
// Notify all subscribers that settings have changed (if requested)
|
||||
if (emitEvent) {
|
||||
if (__DEV__) console.log('Emitting settings change event');
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
Alert,
|
||||
Platform,
|
||||
Clipboard,
|
||||
Linking,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
|
|
@ -28,6 +29,8 @@ import { RootStackParamList } from '../navigation/AppNavigator';
|
|||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { useDownloads } from '../contexts/DownloadsContext';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { VideoPlayerService } from '../services/videoPlayerService';
|
||||
import type { DownloadItem } from '../contexts/DownloadsContext';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
|
|
@ -60,7 +63,7 @@ const optimizePosterUrl = (poster: string | undefined | null): string => {
|
|||
// Empty state component
|
||||
const EmptyDownloadsState: React.FC<{ navigation: NavigationProp<RootStackParamList> }> = ({ navigation }) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
|
||||
return (
|
||||
<View style={styles.emptyContainer}>
|
||||
<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 }]}>
|
||||
Downloaded content will appear here for offline viewing
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
<TouchableOpacity
|
||||
style={[styles.exploreButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={() => {
|
||||
navigation.navigate('Search');
|
||||
|
|
@ -129,12 +132,12 @@ const DownloadItemComponent: React.FC<{
|
|||
|
||||
const formatBytes = (bytes?: number) => {
|
||||
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 v = bytes / Math.pow(1024, i);
|
||||
return `${v.toFixed(v >= 100 ? 0 : v >= 10 ? 1 : 2)} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (item.status) {
|
||||
case 'downloading':
|
||||
|
|
@ -218,10 +221,10 @@ const DownloadItemComponent: React.FC<{
|
|||
<MaterialCommunityIcons
|
||||
name={
|
||||
item.status === 'completed' ? 'check' :
|
||||
item.status === 'downloading' ? 'download' :
|
||||
item.status === 'paused' ? 'pause' :
|
||||
item.status === 'error' ? 'alert-circle' :
|
||||
'clock'
|
||||
item.status === 'downloading' ? 'download' :
|
||||
item.status === 'paused' ? 'pause' :
|
||||
item.status === 'error' ? 'alert-circle' :
|
||||
'clock'
|
||||
}
|
||||
size={12}
|
||||
color="white"
|
||||
|
|
@ -234,10 +237,10 @@ const DownloadItemComponent: React.FC<{
|
|||
<View style={styles.downloadHeader}>
|
||||
<View style={styles.titleContainer}>
|
||||
<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>
|
||||
</View>
|
||||
|
||||
|
||||
{item.type === 'series' && (
|
||||
<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}
|
||||
|
|
@ -293,7 +296,7 @@ const DownloadItemComponent: React.FC<{
|
|||
]}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
<View style={styles.progressDetails}>
|
||||
<Text style={[styles.progressPercentage, { color: currentTheme.colors.text }]}>
|
||||
{item.progress || 0}%
|
||||
|
|
@ -322,7 +325,7 @@ const DownloadItemComponent: React.FC<{
|
|||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
|
||||
onPress={() => onRequestRemove(item)}
|
||||
|
|
@ -342,6 +345,7 @@ const DownloadItemComponent: React.FC<{
|
|||
const DownloadsScreen: React.FC = () => {
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { currentTheme } = useTheme();
|
||||
const { settings } = useSettings();
|
||||
const { top: safeAreaTop } = useSafeAreaInsets();
|
||||
const { downloads, pauseDownload, resumeDownload, cancelDownload } = useDownloads();
|
||||
const { showSuccess, showInfo } = useToast();
|
||||
|
|
@ -394,7 +398,7 @@ const DownloadsScreen: React.FC = () => {
|
|||
setIsRefreshing(false);
|
||||
}, []);
|
||||
|
||||
const handleDownloadPress = useCallback((item: DownloadItem) => {
|
||||
const handleDownloadPress = useCallback(async (item: DownloadItem) => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
if (item.status !== 'completed') {
|
||||
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 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;
|
||||
// Use external player if enabled in settings
|
||||
if (settings.useExternalPlayerForDownloads) {
|
||||
if (Platform.OS === 'android') {
|
||||
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';
|
||||
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);
|
||||
}, [navigation]);
|
||||
if (success) return;
|
||||
// Fall through to internal player if external fails
|
||||
} catch (error) {
|
||||
console.error('External player failed:', error);
|
||||
// Fall through to internal player
|
||||
}
|
||||
} else if (Platform.OS === 'ios') {
|
||||
const streamUrl = encodeURIComponent(uri);
|
||||
let externalPlayerUrls: string[] = [];
|
||||
|
||||
switch (settings.preferredPlayer) {
|
||||
case 'vlc':
|
||||
externalPlayerUrls = [
|
||||
`vlc://${uri}`,
|
||||
`vlc-x-callback://x-callback-url/stream?url=${streamUrl}`,
|
||||
`vlc://${streamUrl}`
|
||||
];
|
||||
break;
|
||||
|
||||
case 'outplayer':
|
||||
externalPlayerUrls = [
|
||||
`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') => {
|
||||
if (action === 'pause') pauseDownload(item.id);
|
||||
|
|
@ -479,8 +582,8 @@ const DownloadsScreen: React.FC = () => {
|
|||
style={[
|
||||
styles.filterButton,
|
||||
{
|
||||
backgroundColor: selectedFilter === filter
|
||||
? currentTheme.colors.primary
|
||||
backgroundColor: selectedFilter === filter
|
||||
? currentTheme.colors.primary
|
||||
: currentTheme.colors.elevation1,
|
||||
}
|
||||
]}
|
||||
|
|
@ -490,8 +593,8 @@ const DownloadsScreen: React.FC = () => {
|
|||
<Text style={[
|
||||
styles.filterButtonText,
|
||||
{
|
||||
color: selectedFilter === filter
|
||||
? currentTheme.colors.white
|
||||
color: selectedFilter === filter
|
||||
? currentTheme.colors.white
|
||||
: currentTheme.colors.text,
|
||||
}
|
||||
]}>
|
||||
|
|
@ -501,16 +604,16 @@ const DownloadsScreen: React.FC = () => {
|
|||
<View style={[
|
||||
styles.filterBadge,
|
||||
{
|
||||
backgroundColor: selectedFilter === filter
|
||||
? currentTheme.colors.white
|
||||
backgroundColor: selectedFilter === filter
|
||||
? currentTheme.colors.white
|
||||
: currentTheme.colors.primary,
|
||||
}
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.filterBadgeText,
|
||||
{
|
||||
color: selectedFilter === filter
|
||||
? currentTheme.colors.primary
|
||||
color: selectedFilter === filter
|
||||
? currentTheme.colors.primary
|
||||
: currentTheme.colors.white,
|
||||
}
|
||||
]}>
|
||||
|
|
@ -534,8 +637,8 @@ const DownloadsScreen: React.FC = () => {
|
|||
styles.header,
|
||||
{
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
paddingTop: (Platform.OS === 'android'
|
||||
? (StatusBar.currentHeight || 0) + 26
|
||||
paddingTop: (Platform.OS === 'android'
|
||||
? (StatusBar.currentHeight || 0) + 26
|
||||
: safeAreaTop + 15) + (isTablet ? 64 : 0),
|
||||
borderBottomColor: currentTheme.colors.border,
|
||||
},
|
||||
|
|
@ -557,7 +660,7 @@ const DownloadsScreen: React.FC = () => {
|
|||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
|
||||
{downloads.length > 0 && (
|
||||
<View style={styles.filterContainer}>
|
||||
{renderFilterButton('all', 'All', stats.total)}
|
||||
|
|
@ -624,10 +727,10 @@ const DownloadsScreen: React.FC = () => {
|
|||
<CustomAlert
|
||||
visible={showRemoveAlert}
|
||||
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={[
|
||||
{ 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); }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
isLast,
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
|
|
@ -175,17 +175,17 @@ const PlayerSettingsScreen: React.FC = () => {
|
|||
Settings
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
||||
<View style={styles.headerActions}>
|
||||
{/* Empty for now, but ready for future actions */}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
||||
Video Player
|
||||
</Text>
|
||||
|
||||
<ScrollView
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
|
|
@ -229,7 +229,7 @@ const PlayerSettingsScreen: React.FC = () => {
|
|||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text
|
||||
style={[
|
||||
|
|
@ -322,6 +322,48 @@ const PlayerSettingsScreen: React.FC = () => {
|
|||
/>
|
||||
</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>
|
||||
</ScrollView>
|
||||
|
|
|
|||
Loading…
Reference in a new issue