mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-26 19:12:54 +00:00
706 lines
No EOL
20 KiB
TypeScript
706 lines
No EOL
20 KiB
TypeScript
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
StatusBar,
|
|
Dimensions,
|
|
TouchableOpacity,
|
|
FlatList,
|
|
RefreshControl,
|
|
Alert,
|
|
Platform,
|
|
} from 'react-native';
|
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
import * as Haptics from 'expo-haptics';
|
|
import { useTheme } from '../contexts/ThemeContext';
|
|
import Animated, {
|
|
useAnimatedStyle,
|
|
useSharedValue,
|
|
withTiming,
|
|
withSpring,
|
|
} from 'react-native-reanimated';
|
|
import { NavigationProp } from '@react-navigation/native';
|
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import { useDownloads } from '../contexts/DownloadsContext';
|
|
import type { DownloadItem } from '../contexts/DownloadsContext';
|
|
|
|
const { height, width } = Dimensions.get('window');
|
|
|
|
// Download items come from DownloadsContext
|
|
|
|
// Empty state component
|
|
const EmptyDownloadsState: React.FC = () => {
|
|
const { currentTheme } = useTheme();
|
|
|
|
return (
|
|
<View style={styles.emptyContainer}>
|
|
<View style={[styles.emptyIconContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
|
<MaterialCommunityIcons
|
|
name="download-outline"
|
|
size={48}
|
|
color={currentTheme.colors.mediumEmphasis}
|
|
/>
|
|
</View>
|
|
<Text style={[styles.emptyTitle, { color: currentTheme.colors.text }]}>
|
|
No Downloads Yet
|
|
</Text>
|
|
<Text style={[styles.emptySubtitle, { color: currentTheme.colors.mediumEmphasis }]}>
|
|
Downloaded content will appear here for offline viewing
|
|
</Text>
|
|
<TouchableOpacity
|
|
style={[styles.exploreButton, { backgroundColor: currentTheme.colors.primary }]}
|
|
onPress={() => {
|
|
// Navigate to search or home to find content
|
|
}}
|
|
>
|
|
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.background }]}>
|
|
Explore Content
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
// Download item component
|
|
const DownloadItemComponent: React.FC<{
|
|
item: DownloadItem;
|
|
onPress: (item: DownloadItem) => void;
|
|
onAction: (item: DownloadItem, action: 'pause' | 'resume' | 'cancel' | 'retry') => void;
|
|
}> = React.memo(({ item, onPress, onAction }) => {
|
|
const { currentTheme } = useTheme();
|
|
const formatBytes = (bytes?: number) => {
|
|
if (!bytes || bytes <= 0) return '0 B';
|
|
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':
|
|
return currentTheme.colors.primary;
|
|
case 'completed':
|
|
return currentTheme.colors.success;
|
|
case 'paused':
|
|
return currentTheme.colors.warning;
|
|
case 'error':
|
|
return currentTheme.colors.error;
|
|
case 'queued':
|
|
return currentTheme.colors.mediumEmphasis;
|
|
default:
|
|
return currentTheme.colors.mediumEmphasis;
|
|
}
|
|
};
|
|
|
|
const getStatusText = () => {
|
|
switch (item.status) {
|
|
case 'downloading':
|
|
const eta = item.etaSeconds ? `${Math.ceil(item.etaSeconds / 60)}m` : undefined;
|
|
return eta ? `Downloading • ${eta}` : 'Downloading';
|
|
case 'completed':
|
|
return 'Completed';
|
|
case 'paused':
|
|
return 'Paused';
|
|
case 'error':
|
|
return 'Error';
|
|
case 'queued':
|
|
return 'Queued';
|
|
default:
|
|
return 'Unknown';
|
|
}
|
|
};
|
|
|
|
const getActionIcon = () => {
|
|
switch (item.status) {
|
|
case 'downloading':
|
|
// return 'pause'; // Resume support commented out
|
|
return null;
|
|
case 'paused':
|
|
case 'error':
|
|
// return 'play'; // Resume support commented out
|
|
return null;
|
|
case 'queued':
|
|
// return 'play'; // Resume support commented out
|
|
return null;
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const handleActionPress = () => {
|
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
|
|
switch (item.status) {
|
|
case 'downloading':
|
|
// onAction(item, 'pause'); // Resume support commented out
|
|
break;
|
|
case 'paused':
|
|
case 'error':
|
|
case 'queued':
|
|
// onAction(item, 'resume'); // Resume support commented out
|
|
break;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
style={[styles.downloadItem, { backgroundColor: currentTheme.colors.card }]}
|
|
onPress={() => onPress(item)}
|
|
activeOpacity={0.8}
|
|
>
|
|
{/* Content info */}
|
|
<View style={styles.downloadContent}>
|
|
<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')}` : ''}
|
|
</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}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
|
|
{/* Progress section */}
|
|
<View style={styles.progressSection}>
|
|
{/* Provider + quality row */}
|
|
<View style={styles.providerRow}>
|
|
<Text style={[styles.providerText, { color: currentTheme.colors.mediumEmphasis }]}>
|
|
{(item.providerName || 'Provider') + (item.quality ? ` ${item.quality}` : '')}
|
|
</Text>
|
|
</View>
|
|
{/* Status row */}
|
|
<View style={styles.statusRow}>
|
|
<Text style={[styles.statusText, { color: getStatusColor() }]}>
|
|
{getStatusText()}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Size row */}
|
|
<View style={styles.sizeRow}>
|
|
<Text style={[styles.progressText, { color: currentTheme.colors.mediumEmphasis }]}>
|
|
{formatBytes(item.downloadedBytes)} / {item.totalBytes ? formatBytes(item.totalBytes) : '—'}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Progress bar */}
|
|
<View style={[styles.progressContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
|
<Animated.View
|
|
style={[
|
|
styles.progressBar,
|
|
{
|
|
backgroundColor: getStatusColor(),
|
|
width: `${item.progress || 0}%`,
|
|
},
|
|
]}
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.progressDetails}>
|
|
<Text style={[styles.progressPercentage, { color: currentTheme.colors.text }]}>
|
|
{item.progress || 0}%
|
|
</Text>
|
|
{item.etaSeconds && item.status === 'downloading' && (
|
|
<Text style={[styles.etaText, { color: currentTheme.colors.mediumEmphasis }]}>
|
|
{Math.ceil(item.etaSeconds / 60)}m remaining
|
|
</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Action buttons */}
|
|
<View style={styles.actionContainer}>
|
|
{getActionIcon() && (
|
|
<TouchableOpacity
|
|
style={[styles.actionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
|
|
onPress={handleActionPress}
|
|
activeOpacity={0.7}
|
|
>
|
|
<MaterialCommunityIcons
|
|
name={getActionIcon() as any}
|
|
size={20}
|
|
color={currentTheme.colors.primary}
|
|
/>
|
|
</TouchableOpacity>
|
|
)}
|
|
|
|
<TouchableOpacity
|
|
style={[styles.actionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
|
|
onPress={() => {
|
|
Alert.alert(
|
|
'Remove Download',
|
|
'Are you sure you want to remove this download?',
|
|
[
|
|
{ text: 'Cancel', style: 'cancel' },
|
|
{ text: 'Remove', style: 'destructive', onPress: () => onAction(item, 'cancel') },
|
|
]
|
|
);
|
|
}}
|
|
activeOpacity={0.7}
|
|
>
|
|
<MaterialCommunityIcons
|
|
name="delete-outline"
|
|
size={20}
|
|
color={currentTheme.colors.error}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</TouchableOpacity>
|
|
);
|
|
});
|
|
|
|
const DownloadsScreen: React.FC = () => {
|
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
|
const { currentTheme } = useTheme();
|
|
const { top: safeAreaTop } = useSafeAreaInsets();
|
|
const { downloads, /*pauseDownload, resumeDownload,*/ cancelDownload } = useDownloads();
|
|
|
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
const [selectedFilter, setSelectedFilter] = useState<'all' | 'downloading' | 'completed'>('all');
|
|
|
|
// Animation values
|
|
const headerOpacity = useSharedValue(1);
|
|
|
|
// Filter downloads based on selected filter
|
|
const filteredDownloads = useMemo(() => {
|
|
if (selectedFilter === 'all') return downloads;
|
|
return downloads.filter(item => {
|
|
switch (selectedFilter) {
|
|
case 'downloading':
|
|
return item.status === 'downloading' || item.status === 'queued';
|
|
case 'completed':
|
|
return item.status === 'completed';
|
|
// case 'paused':
|
|
// return item.status === 'paused' || item.status === 'error'; // Resume support commented out
|
|
default:
|
|
return true;
|
|
}
|
|
});
|
|
}, [downloads, selectedFilter]);
|
|
|
|
// Statistics
|
|
const stats = useMemo(() => {
|
|
const total = downloads.length;
|
|
const downloading = downloads.filter(item =>
|
|
item.status === 'downloading' || item.status === 'queued'
|
|
).length;
|
|
const completed = downloads.filter(item => item.status === 'completed').length;
|
|
// const paused = downloads.filter(item =>
|
|
// item.status === 'paused' || item.status === 'error'
|
|
// ).length; // Resume support commented out
|
|
|
|
return { total, downloading, completed /*, paused*/ };
|
|
}, [downloads]);
|
|
|
|
// Handlers
|
|
const handleRefresh = useCallback(async () => {
|
|
setIsRefreshing(true);
|
|
// In a real app, this would refresh the downloads from the service
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
setIsRefreshing(false);
|
|
}, []);
|
|
|
|
const handleDownloadPress = useCallback((item: DownloadItem) => {
|
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
if (item.status !== 'completed') {
|
|
Alert.alert('Download not ready', 'Please wait until the download completes.');
|
|
return;
|
|
}
|
|
const uri = (item as any).fileUri || (item as any).sourceUrl;
|
|
if (!uri) return;
|
|
|
|
// Infer videoType and mkv
|
|
const lower = String(uri).toLowerCase();
|
|
const isMkv = /\.mkv(\?|$)/i.test(lower) || /(?:[?&]ext=|container=|format=)mkv\b/i.test(lower);
|
|
const isM3u8 = /\.m3u8(\?|$)/i.test(lower);
|
|
const isMpd = /\.mpd(\?|$)/i.test(lower);
|
|
const isMp4 = /\.mp4(\?|$)/i.test(lower);
|
|
const videoType = isM3u8 ? 'm3u8' : isMpd ? 'mpd' : isMp4 ? 'mp4' : 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.id,
|
|
type: item.type,
|
|
episodeId: undefined,
|
|
imdbId: undefined,
|
|
availableStreams: {},
|
|
backdrop: undefined,
|
|
videoType,
|
|
} as any);
|
|
}, [navigation]);
|
|
|
|
const handleDownloadAction = useCallback((item: DownloadItem, action: 'pause' | 'resume' | 'cancel' | 'retry') => {
|
|
// if (action === 'pause') pauseDownload(item.id);
|
|
// if (action === 'resume') resumeDownload(item.id);
|
|
if (action === 'cancel') cancelDownload(item.id);
|
|
}, [/*pauseDownload, resumeDownload,*/ cancelDownload]);
|
|
|
|
const handleFilterPress = useCallback((filter: 'all' | 'downloading' | 'completed') => {
|
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
setSelectedFilter(filter);
|
|
}, []);
|
|
|
|
// Focus effect
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
// In a real app, this would load downloads from the service
|
|
// For now, we'll just show empty state
|
|
}, [])
|
|
);
|
|
|
|
// Animated styles
|
|
const headerStyle = useAnimatedStyle(() => ({
|
|
opacity: headerOpacity.value,
|
|
}));
|
|
|
|
const renderFilterButton = (filter: typeof selectedFilter, label: string, count: number) => (
|
|
<TouchableOpacity
|
|
key={filter}
|
|
style={[
|
|
styles.filterButton,
|
|
{
|
|
backgroundColor: selectedFilter === filter
|
|
? currentTheme.colors.primary
|
|
: currentTheme.colors.elevation1,
|
|
}
|
|
]}
|
|
onPress={() => handleFilterPress(filter)}
|
|
activeOpacity={0.8}
|
|
>
|
|
<Text style={[
|
|
styles.filterButtonText,
|
|
{
|
|
color: selectedFilter === filter
|
|
? currentTheme.colors.background
|
|
: currentTheme.colors.text,
|
|
}
|
|
]}>
|
|
{label}
|
|
</Text>
|
|
{count > 0 && (
|
|
<View style={[
|
|
styles.filterBadge,
|
|
{
|
|
backgroundColor: selectedFilter === filter
|
|
? currentTheme.colors.background
|
|
: currentTheme.colors.primary,
|
|
}
|
|
]}>
|
|
<Text style={[
|
|
styles.filterBadgeText,
|
|
{
|
|
color: selectedFilter === filter
|
|
? currentTheme.colors.primary
|
|
: currentTheme.colors.background,
|
|
}
|
|
]}>
|
|
{count}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</TouchableOpacity>
|
|
);
|
|
|
|
return (
|
|
<View style={[styles.container, { backgroundColor: currentTheme.colors.background }]}>
|
|
<StatusBar
|
|
translucent
|
|
barStyle="light-content"
|
|
backgroundColor="transparent"
|
|
/>
|
|
|
|
{/* Header */}
|
|
<Animated.View style={[
|
|
styles.header,
|
|
{
|
|
backgroundColor: currentTheme.colors.background,
|
|
paddingTop: safeAreaTop + 16,
|
|
},
|
|
headerStyle,
|
|
]}>
|
|
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
|
Downloads
|
|
</Text>
|
|
|
|
{downloads.length > 0 && (
|
|
<View style={styles.filterContainer}>
|
|
{renderFilterButton('all', 'All', stats.total)}
|
|
{renderFilterButton('downloading', 'Active', stats.downloading)}
|
|
{renderFilterButton('completed', 'Done', stats.completed)}
|
|
{/* {renderFilterButton('paused', 'Paused', stats.paused)} */} {/* Resume support commented out */}
|
|
</View>
|
|
)}
|
|
</Animated.View>
|
|
|
|
{/* Content */}
|
|
{downloads.length === 0 ? (
|
|
<EmptyDownloadsState />
|
|
) : (
|
|
<FlatList
|
|
data={filteredDownloads}
|
|
keyExtractor={(item) => item.id}
|
|
renderItem={({ item }) => (
|
|
<DownloadItemComponent
|
|
item={item}
|
|
onPress={handleDownloadPress}
|
|
onAction={handleDownloadAction}
|
|
/>
|
|
)}
|
|
contentContainerStyle={styles.listContainer}
|
|
showsVerticalScrollIndicator={false}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={isRefreshing}
|
|
onRefresh={handleRefresh}
|
|
tintColor={currentTheme.colors.primary}
|
|
colors={[currentTheme.colors.primary]}
|
|
/>
|
|
}
|
|
ListEmptyComponent={() => (
|
|
<View style={styles.emptyFilterContainer}>
|
|
<MaterialCommunityIcons
|
|
name="filter-off"
|
|
size={48}
|
|
color={currentTheme.colors.mediumEmphasis}
|
|
/>
|
|
<Text style={[styles.emptyFilterTitle, { color: currentTheme.colors.text }]}>
|
|
No {selectedFilter} downloads
|
|
</Text>
|
|
<Text style={[styles.emptyFilterSubtitle, { color: currentTheme.colors.mediumEmphasis }]}>
|
|
Try selecting a different filter
|
|
</Text>
|
|
</View>
|
|
)}
|
|
/>
|
|
)}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
},
|
|
header: {
|
|
paddingHorizontal: 20,
|
|
paddingBottom: 16,
|
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
|
|
},
|
|
headerTitle: {
|
|
fontSize: 32,
|
|
fontWeight: '700',
|
|
marginBottom: 16,
|
|
},
|
|
filterContainer: {
|
|
flexDirection: 'row',
|
|
gap: 12,
|
|
},
|
|
filterButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 8,
|
|
borderRadius: 20,
|
|
gap: 8,
|
|
},
|
|
filterButtonText: {
|
|
fontSize: 14,
|
|
fontWeight: '600',
|
|
},
|
|
filterBadge: {
|
|
minWidth: 20,
|
|
height: 20,
|
|
borderRadius: 10,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
paddingHorizontal: 6,
|
|
},
|
|
filterBadgeText: {
|
|
fontSize: 12,
|
|
fontWeight: '700',
|
|
},
|
|
listContainer: {
|
|
padding: 20,
|
|
paddingTop: 8,
|
|
},
|
|
downloadItem: {
|
|
borderRadius: 16,
|
|
padding: 16,
|
|
marginBottom: 12,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 8,
|
|
elevation: 3,
|
|
},
|
|
downloadContent: {
|
|
flex: 1,
|
|
marginRight: 12,
|
|
},
|
|
downloadHeader: {
|
|
marginBottom: 12,
|
|
},
|
|
titleContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginBottom: 4,
|
|
gap: 8,
|
|
},
|
|
downloadTitle: {
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
flex: 1,
|
|
},
|
|
qualityBadge: {
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 2,
|
|
borderRadius: 6,
|
|
},
|
|
qualityText: {
|
|
fontSize: 12,
|
|
fontWeight: '700',
|
|
},
|
|
episodeInfo: {
|
|
fontSize: 14,
|
|
fontWeight: '500',
|
|
},
|
|
progressSection: {
|
|
gap: 4,
|
|
},
|
|
providerRow: {
|
|
marginBottom: 2,
|
|
},
|
|
providerText: {
|
|
fontSize: 12,
|
|
fontWeight: '500',
|
|
},
|
|
statusRow: {
|
|
marginBottom: 2,
|
|
},
|
|
sizeRow: {
|
|
marginBottom: 6,
|
|
},
|
|
progressInfo: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
flexWrap: 'wrap',
|
|
gap: 8,
|
|
},
|
|
statusText: {
|
|
fontSize: 13,
|
|
fontWeight: '600',
|
|
},
|
|
progressText: {
|
|
fontSize: 12,
|
|
fontWeight: '500',
|
|
},
|
|
progressContainer: {
|
|
height: 4,
|
|
borderRadius: 2,
|
|
overflow: 'hidden',
|
|
},
|
|
progressBar: {
|
|
height: '100%',
|
|
borderRadius: 2,
|
|
},
|
|
progressDetails: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
},
|
|
progressPercentage: {
|
|
fontSize: 14,
|
|
fontWeight: '700',
|
|
},
|
|
etaText: {
|
|
fontSize: 12,
|
|
fontWeight: '500',
|
|
},
|
|
actionContainer: {
|
|
flexDirection: 'row',
|
|
gap: 8,
|
|
},
|
|
actionButton: {
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 20,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
emptyContainer: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
paddingHorizontal: 40,
|
|
},
|
|
emptyIconContainer: {
|
|
width: 96,
|
|
height: 96,
|
|
borderRadius: 48,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginBottom: 24,
|
|
},
|
|
emptyTitle: {
|
|
fontSize: 24,
|
|
fontWeight: '700',
|
|
marginBottom: 8,
|
|
textAlign: 'center',
|
|
},
|
|
emptySubtitle: {
|
|
fontSize: 16,
|
|
textAlign: 'center',
|
|
lineHeight: 24,
|
|
marginBottom: 32,
|
|
},
|
|
exploreButton: {
|
|
paddingHorizontal: 24,
|
|
paddingVertical: 12,
|
|
borderRadius: 24,
|
|
},
|
|
exploreButtonText: {
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
},
|
|
emptyFilterContainer: {
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
paddingVertical: 60,
|
|
},
|
|
emptyFilterTitle: {
|
|
fontSize: 18,
|
|
fontWeight: '600',
|
|
marginTop: 16,
|
|
marginBottom: 8,
|
|
},
|
|
emptyFilterSubtitle: {
|
|
fontSize: 14,
|
|
textAlign: 'center',
|
|
},
|
|
});
|
|
|
|
export default DownloadsScreen; |