Enhance navigation and layout consistency across the app; integrate native screens for improved performance, update header visibility logic in NuvioHeader, and implement fixed layout dimensions in AppNavigator. Refactor screens to ensure consistent status bar settings and header spacing, while optimizing content rendering in DiscoverScreen, LibraryScreen, and SettingsScreen for better user experience.

This commit is contained in:
tapframe 2025-04-26 15:31:06 +05:30
parent b1e1017288
commit 78583c8e80
7 changed files with 637 additions and 401 deletions

10
App.tsx
View file

@ -14,6 +14,7 @@ import { NavigationContainer } from '@react-navigation/native';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { Provider as PaperProvider } from 'react-native-paper'; import { Provider as PaperProvider } from 'react-native-paper';
import { enableScreens } from 'react-native-screens';
import AppNavigator, { import AppNavigator, {
CustomNavigationDarkTheme, CustomNavigationDarkTheme,
CustomDarkTheme CustomDarkTheme
@ -23,6 +24,9 @@ import { CatalogProvider } from './src/contexts/CatalogContext';
import { GenreProvider } from './src/contexts/GenreContext'; import { GenreProvider } from './src/contexts/GenreContext';
import { TraktProvider } from './src/contexts/TraktContext'; import { TraktProvider } from './src/contexts/TraktContext';
// This fixes many navigation layout issues by using native screen containers
enableScreens(true);
function App(): React.JSX.Element { function App(): React.JSX.Element {
// Always use dark mode // Always use dark mode
const isDarkMode = true; const isDarkMode = true;
@ -33,7 +37,11 @@ function App(): React.JSX.Element {
<CatalogProvider> <CatalogProvider>
<TraktProvider> <TraktProvider>
<PaperProvider theme={CustomDarkTheme}> <PaperProvider theme={CustomDarkTheme}>
<NavigationContainer theme={CustomNavigationDarkTheme}> <NavigationContainer
theme={CustomNavigationDarkTheme}
// Disable automatic linking which can cause layout issues
linking={undefined}
>
<View style={[styles.container, { backgroundColor: '#000000' }]}> <View style={[styles.container, { backgroundColor: '#000000' }]}>
<StatusBar <StatusBar
style="light" style="light"

View file

@ -2,7 +2,7 @@ import React from 'react';
import { View, TouchableOpacity, Platform, StyleSheet, Image } from 'react-native'; import { View, TouchableOpacity, Platform, StyleSheet, Image } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors } from '../styles/colors'; import { colors } from '../styles/colors';
import { useNavigation } from '@react-navigation/native'; import { useNavigation, useRoute } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { RootStackParamList } from '../navigation/AppNavigator'; import type { RootStackParamList } from '../navigation/AppNavigator';
import { BlurView as ExpoBlurView } from 'expo-blur'; import { BlurView as ExpoBlurView } from 'expo-blur';
@ -13,6 +13,12 @@ type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
export const NuvioHeader = () => { export const NuvioHeader = () => {
const navigation = useNavigation<NavigationProp>(); const navigation = useNavigation<NavigationProp>();
const route = useRoute();
// Only render the header if the current route is 'Home'
if (route.name !== 'Home') {
return null;
}
// Determine if running in Expo Go // Determine if running in Expo Go
const isExpoGo = Constants.executionEnvironment === ExecutionEnvironment.StoreClient; const isExpoGo = Constants.executionEnvironment === ExecutionEnvironment.StoreClient;

View file

@ -2,7 +2,7 @@ import React, { useEffect, useRef } from 'react';
import { NavigationContainer, DefaultTheme as NavigationDefaultTheme, DarkTheme as NavigationDarkTheme, Theme, NavigationProp } from '@react-navigation/native'; import { NavigationContainer, DefaultTheme as NavigationDefaultTheme, DarkTheme as NavigationDarkTheme, Theme, NavigationProp } from '@react-navigation/native';
import { createNativeStackNavigator, NativeStackNavigationOptions, NativeStackNavigationProp } from '@react-navigation/native-stack'; import { createNativeStackNavigator, NativeStackNavigationOptions, NativeStackNavigationProp } from '@react-navigation/native-stack';
import { createBottomTabNavigator, BottomTabNavigationProp } from '@react-navigation/bottom-tabs'; import { createBottomTabNavigator, BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
import { useColorScheme, Platform, Animated, StatusBar, TouchableOpacity, View, Text } from 'react-native'; import { useColorScheme, Platform, Animated, StatusBar, TouchableOpacity, View, Text, AppState } from 'react-native';
import { PaperProvider, MD3DarkTheme, MD3LightTheme, adaptNavigationTheme } from 'react-native-paper'; import { PaperProvider, MD3DarkTheme, MD3LightTheme, adaptNavigationTheme } from 'react-native-paper';
import type { MD3Theme } from 'react-native-paper'; import type { MD3Theme } from 'react-native-paper';
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs'; import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
@ -12,6 +12,7 @@ import { BlurView } from 'expo-blur';
import { colors } from '../styles/colors'; import { colors } from '../styles/colors';
import { NuvioHeader } from '../components/NuvioHeader'; import { NuvioHeader } from '../components/NuvioHeader';
import { Stream } from '../types/streams'; import { Stream } from '../types/streams';
import { SafeAreaProvider } from 'react-native-safe-area-context';
// Import screens with their proper types // Import screens with their proper types
import HomeScreen from '../screens/HomeScreen'; import HomeScreen from '../screens/HomeScreen';
@ -320,6 +321,65 @@ const TabIcon = React.memo(({ focused, color, iconName }: {
); );
}); });
// Update the TabScreenWrapper component with fixed layout dimensions
const TabScreenWrapper: React.FC<{children: React.ReactNode}> = ({ children }) => {
// Force consistent status bar settings
useEffect(() => {
const applyStatusBarConfig = () => {
StatusBar.setBarStyle('light-content');
StatusBar.setTranslucent(true);
StatusBar.setBackgroundColor('transparent');
};
applyStatusBarConfig();
// Apply status bar config on every focus
const subscription = Platform.OS === 'android'
? AppState.addEventListener('change', (state) => {
if (state === 'active') {
applyStatusBarConfig();
}
})
: { remove: () => {} };
return () => {
subscription.remove();
};
}, []);
return (
<View style={{
flex: 1,
backgroundColor: colors.darkBackground,
// Lock the layout to prevent shifts
position: 'relative',
overflow: 'hidden'
}}>
{/* Reserve consistent space for the header area on all screens */}
<View style={{
height: Platform.OS === 'android' ? 80 : 60,
width: '100%',
backgroundColor: colors.darkBackground,
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: -1
}} />
{children}
</View>
);
};
// Add this component to wrap each screen in the tab navigator
const WrappedScreen: React.FC<{Screen: React.ComponentType<any>}> = ({ Screen }) => {
return (
<TabScreenWrapper>
<Screen />
</TabScreenWrapper>
);
};
// Tab Navigator // Tab Navigator
const MainTabs = () => { const MainTabs = () => {
// Always use dark mode // Always use dark mode
@ -454,112 +514,138 @@ const MainTabs = () => {
}; };
return ( return (
<Tab.Navigator <View style={{ flex: 1, backgroundColor: colors.darkBackground }}>
tabBar={renderTabBar} {/* Common StatusBar for all tabs */}
screenOptions={({ route }) => ({ <StatusBar
tabBarIcon: ({ focused, color, size }) => { translucent
let iconName: IconNameType = 'home'; barStyle="light-content"
backgroundColor="transparent"
/>
switch (route.name) { <Tab.Navigator
case 'Home': tabBar={renderTabBar}
iconName = 'home'; screenOptions={({ route }) => ({
break; tabBarIcon: ({ focused, color, size }) => {
case 'Discover': let iconName: IconNameType = 'home';
iconName = 'compass';
break;
case 'Library':
iconName = 'play-box-multiple';
break;
case 'Settings':
iconName = 'cog';
break;
}
return <TabIcon focused={focused} color={color} iconName={iconName} />; switch (route.name) {
}, case 'Home':
tabBarActiveTintColor: colors.primary, iconName = 'home';
tabBarInactiveTintColor: '#FFFFFF', break;
tabBarStyle: { case 'Discover':
position: 'absolute', iconName = 'compass';
backgroundColor: 'transparent', break;
borderTopWidth: 0, case 'Library':
elevation: 0, iconName = 'play-box-multiple';
height: 85, break;
paddingBottom: 20, case 'Settings':
paddingTop: 12, iconName = 'cog';
}, break;
tabBarLabelStyle: { }
fontSize: 12,
fontWeight: '600', return <TabIcon focused={focused} color={color} iconName={iconName} />;
marginTop: 0, },
}, tabBarActiveTintColor: colors.primary,
tabBarBackground: () => ( tabBarInactiveTintColor: '#FFFFFF',
Platform.OS === 'ios' ? ( tabBarStyle: {
<BlurView position: 'absolute',
tint="dark" backgroundColor: 'transparent',
intensity={75} borderTopWidth: 0,
style={{ elevation: 0,
position: 'absolute', height: 85,
height: '100%', paddingBottom: 20,
width: '100%', paddingTop: 12,
borderTopColor: 'rgba(255,255,255,0.2)', },
borderTopWidth: 0.5, tabBarLabelStyle: {
shadowColor: '#000', fontSize: 12,
shadowOffset: { width: 0, height: -2 }, fontWeight: '600',
shadowOpacity: 0.1, marginTop: 0,
shadowRadius: 3, },
}} // Completely disable animations between tabs for better performance
/> animationEnabled: false,
) : ( // Keep all screens mounted and active
<LinearGradient lazy: false,
colors={[ freezeOnBlur: false,
'rgba(0, 0, 0, 0)', detachPreviousScreen: false,
'rgba(0, 0, 0, 0.65)', // Configure how the screen renders
'rgba(0, 0, 0, 0.85)', detachInactiveScreens: false,
'rgba(0, 0, 0, 0.98)', tabBarBackground: () => (
]} Platform.OS === 'ios' ? (
locations={[0, 0.2, 0.4, 0.8]} <BlurView
style={{ tint="dark"
position: 'absolute', intensity={75}
height: '100%', style={{
width: '100%', position: 'absolute',
}} height: '100%',
/> width: '100%',
) borderTopColor: 'rgba(255,255,255,0.2)',
), borderTopWidth: 0.5,
header: () => route.name === 'Home' ? <NuvioHeader /> : null, shadowColor: '#000',
headerShown: route.name === 'Home', shadowOffset: { width: 0, height: -2 },
})} shadowOpacity: 0.1,
> shadowRadius: 3,
<Tab.Screen }}
name="Home" />
component={HomeScreen as any} ) : (
options={{ <LinearGradient
tabBarLabel: 'Home', colors={[
}} 'rgba(0, 0, 0, 0)',
/> 'rgba(0, 0, 0, 0.65)',
<Tab.Screen 'rgba(0, 0, 0, 0.85)',
name="Discover" 'rgba(0, 0, 0, 0.98)',
component={DiscoverScreen as any} ]}
options={{ locations={[0, 0.2, 0.4, 0.8]}
tabBarLabel: 'Discover' style={{
}} position: 'absolute',
/> height: '100%',
<Tab.Screen width: '100%',
name="Library" }}
component={LibraryScreen as any} />
options={{ )
tabBarLabel: 'Library' ),
}} header: () => route.name === 'Home' ? <NuvioHeader /> : null,
/> headerShown: route.name === 'Home',
<Tab.Screen // Add fixed screen styling to help with consistency
name="Settings" contentStyle: {
component={SettingsScreen as any} backgroundColor: colors.darkBackground,
options={{ },
tabBarLabel: 'Settings' })}
}} // Global configuration for the tab navigator
/> detachInactiveScreens={false}
</Tab.Navigator> >
<Tab.Screen
name="Home"
component={HomeScreen}
options={{
tabBarLabel: 'Home',
}}
/>
<Tab.Screen
name="Discover"
component={DiscoverScreen}
options={{
tabBarLabel: 'Discover',
headerShown: false
}}
/>
<Tab.Screen
name="Library"
component={LibraryScreen}
options={{
tabBarLabel: 'Library',
headerShown: false
}}
/>
<Tab.Screen
name="Settings"
component={SettingsScreen}
options={{
tabBarLabel: 'Settings',
headerShown: false
}}
/>
</Tab.Navigator>
</View>
); );
}; };
@ -569,7 +655,7 @@ const AppNavigator = () => {
const isDarkMode = true; const isDarkMode = true;
return ( return (
<> <SafeAreaProvider>
<StatusBar <StatusBar
translucent translucent
backgroundColor="transparent" backgroundColor="transparent"
@ -579,7 +665,12 @@ const AppNavigator = () => {
<Stack.Navigator <Stack.Navigator
screenOptions={{ screenOptions={{
headerShown: false, headerShown: false,
animation: Platform.OS === 'android' ? 'fade_from_bottom' : 'default', // Disable animations for smoother transitions
animation: 'none',
// Ensure content is not popping in and out
contentStyle: {
backgroundColor: colors.darkBackground,
}
}} }}
> >
<Stack.Screen <Stack.Screen
@ -734,7 +825,7 @@ const AppNavigator = () => {
/> />
</Stack.Navigator> </Stack.Navigator>
</PaperProvider> </PaperProvider>
</> </SafeAreaProvider>
); );
}; };

View file

@ -24,6 +24,7 @@ import { LinearGradient } from 'expo-linear-gradient';
import { RootStackParamList } from '../navigation/AppNavigator'; import { RootStackParamList } from '../navigation/AppNavigator';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
import { BlurView } from 'expo-blur'; import { BlurView } from 'expo-blur';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
interface Category { interface Category {
id: string; id: string;
@ -281,10 +282,24 @@ const useStyles = () => {
flex: 1, flex: 1,
backgroundColor: colors.darkBackground, backgroundColor: colors.darkBackground,
}, },
headerBackground: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
backgroundColor: colors.darkBackground,
zIndex: 1,
},
contentContainer: {
flex: 1,
backgroundColor: colors.darkBackground,
},
header: { header: {
paddingHorizontal: 20, paddingHorizontal: 20,
paddingVertical: 16, justifyContent: 'flex-end',
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 16 : 16, paddingBottom: 8,
backgroundColor: 'transparent',
zIndex: 2,
}, },
headerContent: { headerContent: {
flexDirection: 'row', flexDirection: 'row',
@ -487,6 +502,24 @@ const DiscoverScreen = () => {
const [allContent, setAllContent] = useState<StreamingContent[]>([]); const [allContent, setAllContent] = useState<StreamingContent[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const styles = useStyles(); const styles = useStyles();
const insets = useSafeAreaInsets();
// Force consistent status bar settings
useEffect(() => {
const applyStatusBarConfig = () => {
StatusBar.setBarStyle('light-content');
if (Platform.OS === 'android') {
StatusBar.setTranslucent(true);
StatusBar.setBackgroundColor('transparent');
}
};
applyStatusBarConfig();
// Re-apply on focus
const unsubscribe = navigation.addListener('focus', applyStatusBarConfig);
return unsubscribe;
}, [navigation]);
// Load content when category or genre changes // Load content when category or genre changes
useEffect(() => { useEffect(() => {
@ -580,17 +613,18 @@ const DiscoverScreen = () => {
// Memoize list key extractor // Memoize list key extractor
const catalogKeyExtractor = useCallback((item: GenreCatalog) => item.genre, []); const catalogKeyExtractor = useCallback((item: GenreCatalog) => item.genre, []);
const headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top;
const headerHeight = headerBaseHeight + topSpacing;
return ( return (
<SafeAreaView style={styles.container}> <View style={styles.container}>
<StatusBar {/* Fixed position header background to prevent shifts */}
barStyle="light-content" <View style={[styles.headerBackground, { height: headerHeight }]} />
backgroundColor="transparent"
translucent
/>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
{/* Header Section */} {/* Header Section with proper top spacing */}
<View style={styles.header}> <View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
<View style={styles.headerContent}> <View style={styles.headerContent}>
<Text style={styles.headerTitle}>Discover</Text> <Text style={styles.headerTitle}>Discover</Text>
<TouchableOpacity <TouchableOpacity
@ -607,66 +641,69 @@ const DiscoverScreen = () => {
</View> </View>
</View> </View>
{/* Categories Section */} {/* Rest of the content */}
<View style={styles.categoryContainer}> <View style={styles.contentContainer}>
<View style={styles.categoriesContent}> {/* Categories Section */}
{CATEGORIES.map((category) => ( <View style={styles.categoryContainer}>
<CategoryButton <View style={styles.categoriesContent}>
key={category.id} {CATEGORIES.map((category) => (
category={category} <CategoryButton
isSelected={selectedCategory.id === category.id} key={category.id}
onPress={() => handleCategoryPress(category)} category={category}
/> isSelected={selectedCategory.id === category.id}
))} onPress={() => handleCategoryPress(category)}
/>
))}
</View>
</View> </View>
</View>
{/* Genres Section */} {/* Genres Section */}
<View style={styles.genreContainer}> <View style={styles.genreContainer}>
<ScrollView <ScrollView
horizontal horizontal
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.genresScrollView} contentContainerStyle={styles.genresScrollView}
decelerationRate="fast" decelerationRate="fast"
snapToInterval={10} snapToInterval={10}
> >
{COMMON_GENRES.map(genre => ( {COMMON_GENRES.map(genre => (
<GenreButton <GenreButton
key={genre} key={genre}
genre={genre} genre={genre}
isSelected={selectedGenre === genre} isSelected={selectedGenre === genre}
onPress={() => handleGenrePress(genre)} onPress={() => handleGenrePress(genre)}
/> />
))} ))}
</ScrollView> </ScrollView>
</View> </View>
{/* Content Section */} {/* Content Section */}
{loading ? ( {loading ? (
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} /> <ActivityIndicator size="large" color={colors.primary} />
</View> </View>
) : catalogs.length > 0 ? ( ) : catalogs.length > 0 ? (
<FlatList <FlatList
data={catalogs} data={catalogs}
renderItem={renderCatalogItem} renderItem={renderCatalogItem}
keyExtractor={catalogKeyExtractor} keyExtractor={catalogKeyExtractor}
contentContainerStyle={styles.catalogsContainer} contentContainerStyle={styles.catalogsContainer}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
initialNumToRender={3} initialNumToRender={3}
maxToRenderPerBatch={3} maxToRenderPerBatch={3}
windowSize={5} windowSize={5}
removeClippedSubviews={Platform.OS === 'android'} removeClippedSubviews={Platform.OS === 'android'}
/> />
) : ( ) : (
<View style={styles.emptyContainer}> <View style={styles.emptyContainer}>
<Text style={styles.emptyText}> <Text style={styles.emptyText}>
No content found for {selectedGenre !== 'All' ? selectedGenre : 'these filters'} No content found for {selectedGenre !== 'All' ? selectedGenre : 'these filters'}
</Text> </Text>
</View> </View>
)} )}
</View>
</View> </View>
</SafeAreaView> </View>
); );
}; };

View file

@ -417,8 +417,8 @@ const HomeScreen = () => {
useCallback(() => { useCallback(() => {
const statusBarConfig = () => { const statusBarConfig = () => {
StatusBar.setBarStyle("light-content"); StatusBar.setBarStyle("light-content");
StatusBar.setTranslucent(true); StatusBar.setTranslucent(true);
StatusBar.setBackgroundColor('transparent'); StatusBar.setBackgroundColor('transparent');
}; };
statusBarConfig(); statusBarConfig();
@ -745,9 +745,9 @@ const HomeScreen = () => {
</Animated.View> </Animated.View>
{hasContinueWatching && ( {hasContinueWatching && (
<Animated.View entering={FadeIn.duration(400).delay(250)}> <Animated.View entering={FadeIn.duration(400).delay(250)}>
<ContinueWatchingSection ref={continueWatchingRef} /> <ContinueWatchingSection ref={continueWatchingRef} />
</Animated.View> </Animated.View>
)} )}
{catalogs.length > 0 ? ( {catalogs.length > 0 ? (

View file

@ -24,6 +24,7 @@ import { catalogService } from '../services/catalogService';
import type { StreamingContent } from '../services/catalogService'; import type { StreamingContent } from '../services/catalogService';
import { RootStackParamList } from '../navigation/AppNavigator'; import { RootStackParamList } from '../navigation/AppNavigator';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
// Types // Types
interface LibraryItem extends StreamingContent { interface LibraryItem extends StreamingContent {
@ -97,6 +98,24 @@ const LibraryScreen = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [libraryItems, setLibraryItems] = useState<LibraryItem[]>([]); const [libraryItems, setLibraryItems] = useState<LibraryItem[]>([]);
const [filter, setFilter] = useState<'all' | 'movies' | 'series'>('all'); const [filter, setFilter] = useState<'all' | 'movies' | 'series'>('all');
const insets = useSafeAreaInsets();
// Force consistent status bar settings
useEffect(() => {
const applyStatusBarConfig = () => {
StatusBar.setBarStyle('light-content');
if (Platform.OS === 'android') {
StatusBar.setTranslucent(true);
StatusBar.setBackgroundColor('transparent');
}
};
applyStatusBarConfig();
// Re-apply on focus
const unsubscribe = navigation.addListener('focus', applyStatusBarConfig);
return unsubscribe;
}, [navigation]);
useEffect(() => { useEffect(() => {
const loadLibrary = async () => { const loadLibrary = async () => {
@ -216,64 +235,71 @@ const LibraryScreen = () => {
); );
}; };
const headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top;
const headerHeight = headerBaseHeight + topSpacing;
return ( return (
<SafeAreaView style={styles.container}> <View style={styles.container}>
<StatusBar {/* Fixed position header background to prevent shifts */}
barStyle="light-content" <View style={[styles.headerBackground, { height: headerHeight }]} />
backgroundColor="transparent"
translucent
/>
<View style={styles.header}> <View style={{ flex: 1 }}>
<View style={styles.headerContent}> {/* Header Section with proper top spacing */}
<Text style={styles.headerTitle}>Library</Text> <View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
<View style={styles.headerContent}>
<Text style={styles.headerTitle}>Library</Text>
</View>
</View>
{/* Content Container */}
<View style={styles.contentContainer}>
<View style={styles.filtersContainer}>
{renderFilter('all', 'All', 'apps')}
{renderFilter('movies', 'Movies', 'movie')}
{renderFilter('series', 'TV Shows', 'live-tv')}
</View>
{loading ? (
<SkeletonLoader />
) : filteredItems.length === 0 ? (
<View style={styles.emptyContainer}>
<MaterialIcons
name="video-library"
size={80}
color={colors.mediumGray}
style={{ opacity: 0.7 }}
/>
<Text style={styles.emptyText}>Your library is empty</Text>
<Text style={styles.emptySubtext}>
Add content to your library to keep track of what you're watching
</Text>
<TouchableOpacity
style={styles.exploreButton}
onPress={() => navigation.navigate('Discover')}
activeOpacity={0.7}
>
<Text style={styles.exploreButtonText}>Explore Content</Text>
</TouchableOpacity>
</View>
) : (
<FlatList
data={filteredItems}
renderItem={renderItem}
keyExtractor={item => item.id}
numColumns={2}
contentContainerStyle={styles.listContainer}
showsVerticalScrollIndicator={false}
columnWrapperStyle={styles.columnWrapper}
initialNumToRender={6}
maxToRenderPerBatch={6}
windowSize={5}
removeClippedSubviews={Platform.OS === 'android'}
/>
)}
</View> </View>
</View> </View>
</View>
<View style={styles.filtersContainer}>
{renderFilter('all', 'All', 'apps')}
{renderFilter('movies', 'Movies', 'movie')}
{renderFilter('series', 'TV Shows', 'live-tv')}
</View>
{loading ? (
<SkeletonLoader />
) : filteredItems.length === 0 ? (
<View style={styles.emptyContainer}>
<MaterialIcons
name="video-library"
size={80}
color={colors.mediumGray}
style={{ opacity: 0.7 }}
/>
<Text style={styles.emptyText}>Your library is empty</Text>
<Text style={styles.emptySubtext}>
Add content to your library to keep track of what you're watching
</Text>
<TouchableOpacity
style={styles.exploreButton}
onPress={() => navigation.navigate('Discover')}
activeOpacity={0.7}
>
<Text style={styles.exploreButtonText}>Explore Content</Text>
</TouchableOpacity>
</View>
) : (
<FlatList
data={filteredItems}
renderItem={renderItem}
keyExtractor={item => item.id}
numColumns={2}
contentContainerStyle={styles.listContainer}
showsVerticalScrollIndicator={false}
columnWrapperStyle={styles.columnWrapper}
initialNumToRender={6}
maxToRenderPerBatch={6}
windowSize={5}
removeClippedSubviews={Platform.OS === 'android'}
/>
)}
</SafeAreaView>
); );
}; };
@ -282,10 +308,24 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
backgroundColor: colors.darkBackground, backgroundColor: colors.darkBackground,
}, },
headerBackground: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
backgroundColor: colors.darkBackground,
zIndex: 1,
},
contentContainer: {
flex: 1,
backgroundColor: colors.darkBackground,
},
header: { header: {
paddingHorizontal: 20, paddingHorizontal: 20,
paddingVertical: 16, justifyContent: 'flex-end',
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 16 : 16, paddingBottom: 8,
backgroundColor: 'transparent',
zIndex: 2,
}, },
headerContent: { headerContent: {
flexDirection: 'row', flexDirection: 'row',

View file

@ -26,6 +26,7 @@ import { stremioService } from '../services/stremioService';
import { useCatalogContext } from '../contexts/CatalogContext'; import { useCatalogContext } from '../contexts/CatalogContext';
import { useTraktContext } from '../contexts/TraktContext'; import { useTraktContext } from '../contexts/TraktContext';
import { catalogService, DataSource } from '../services/catalogService'; import { catalogService, DataSource } from '../services/catalogService';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
@ -125,6 +126,7 @@ const SettingsScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { lastUpdate } = useCatalogContext(); const { lastUpdate } = useCatalogContext();
const { isAuthenticated, userProfile } = useTraktContext(); const { isAuthenticated, userProfile } = useTraktContext();
const insets = useSafeAreaInsets();
// States for dynamic content // States for dynamic content
const [addonCount, setAddonCount] = useState<number>(0); const [addonCount, setAddonCount] = useState<number>(0);
@ -132,6 +134,23 @@ const SettingsScreen: React.FC = () => {
const [mdblistKeySet, setMdblistKeySet] = useState<boolean>(false); const [mdblistKeySet, setMdblistKeySet] = useState<boolean>(false);
const [discoverDataSource, setDiscoverDataSource] = useState<DataSource>(DataSource.STREMIO_ADDONS); const [discoverDataSource, setDiscoverDataSource] = useState<DataSource>(DataSource.STREMIO_ADDONS);
// Force consistent status bar settings
useEffect(() => {
const applyStatusBarConfig = () => {
StatusBar.setBarStyle('light-content');
if (Platform.OS === 'android') {
StatusBar.setTranslucent(true);
StatusBar.setBackgroundColor('transparent');
}
};
applyStatusBarConfig();
// Re-apply on focus
const unsubscribe = navigation.addListener('focus', applyStatusBarConfig);
return unsubscribe;
}, [navigation]);
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
try { try {
// Load addon count and get their catalogs // Load addon count and get their catalogs
@ -231,166 +250,182 @@ const SettingsScreen: React.FC = () => {
await catalogService.setDataSourcePreference(dataSource); await catalogService.setDataSourcePreference(dataSource);
}, []); }, []);
const headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top;
const headerHeight = headerBaseHeight + topSpacing;
return ( return (
<SafeAreaView style={[ <View style={[
styles.container, styles.container,
{ backgroundColor: isDarkMode ? colors.darkBackground : '#F2F2F7' } { backgroundColor: isDarkMode ? colors.darkBackground : '#F2F2F7' }
]}> ]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} /> {/* Fixed position header background to prevent shifts */}
<View style={styles.header}> <View style={[
<Text style={[styles.headerTitle, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}> styles.headerBackground,
Settings { height: headerHeight, backgroundColor: isDarkMode ? colors.darkBackground : '#F2F2F7' }
</Text> ]} />
<TouchableOpacity onPress={handleResetSettings} style={styles.resetButton}>
<Text style={[styles.resetButtonText, {color: colors.primary}]}>Reset</Text>
</TouchableOpacity>
</View>
<ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
>
<SettingsCard isDarkMode={isDarkMode} title="User & Account">
<SettingItem
title="Trakt"
description={isAuthenticated ? `Connected as ${userProfile?.username || 'User'}` : "Not Connected"}
icon="person"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('TraktSettings')}
isLast={true}
/>
</SettingsCard>
<SettingsCard isDarkMode={isDarkMode} title="Features"> <View style={{ flex: 1 }}>
<SettingItem {/* Header Section with proper top spacing */}
title="Calendar" <View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
description="Manage your show calendar settings" <Text style={[styles.headerTitle, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
icon="calendar-today" Settings
renderControl={ChevronRight}
onPress={() => navigation.navigate('Calendar')}
isDarkMode={isDarkMode}
/>
<SettingItem
title="Notifications"
description="Configure episode notifications and reminders"
icon="notifications"
renderControl={ChevronRight}
onPress={() => navigation.navigate('NotificationSettings')}
isDarkMode={isDarkMode}
isLast={true}
/>
</SettingsCard>
<SettingsCard isDarkMode={isDarkMode} title="Content">
<SettingItem
title="Addons"
description="Manage your installed addons"
icon="extension"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('Addons')}
badge={addonCount}
/>
<SettingItem
title="Catalogs"
description="Configure content sources"
icon="view-list"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('CatalogSettings')}
badge={catalogCount}
/>
<SettingItem
title="Home Screen"
description="Customize layout and content"
icon="home"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('HomeScreenSettings')}
/>
<SettingItem
title="Ratings Source"
description={mdblistKeySet ? "MDBList API Configured" : "Configure MDBList API"}
icon="info-outline"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('MDBListSettings')}
/>
<SettingItem
title="TMDB"
description="API & Metadata Settings"
icon="movie-filter"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('TMDBSettings')}
isLast={true}
/>
</SettingsCard>
<SettingsCard isDarkMode={isDarkMode} title="Playback">
<SettingItem
title="Video Player"
description={Platform.OS === 'ios'
? (settings.preferredPlayer === 'internal'
? 'Built-in Player'
: settings.preferredPlayer
? settings.preferredPlayer.toUpperCase()
: 'Built-in Player')
: (settings.useExternalPlayer ? 'External Player' : 'Built-in Player')
}
icon="play-arrow"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('PlayerSettings')}
isLast={true}
/>
</SettingsCard>
<SettingsCard isDarkMode={isDarkMode} title="Discover">
<SettingItem
title="Content Source"
description="Choose where to get content for the Discover screen"
icon="explore"
isDarkMode={isDarkMode}
renderControl={() => (
<View style={styles.selectorContainer}>
<TouchableOpacity
style={[
styles.selectorButton,
discoverDataSource === DataSource.STREMIO_ADDONS && styles.selectorButtonActive
]}
onPress={() => handleDiscoverDataSourceChange(DataSource.STREMIO_ADDONS)}
>
<Text style={[
styles.selectorText,
discoverDataSource === DataSource.STREMIO_ADDONS && styles.selectorTextActive
]}>Addons</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.selectorButton,
discoverDataSource === DataSource.TMDB && styles.selectorButtonActive
]}
onPress={() => handleDiscoverDataSourceChange(DataSource.TMDB)}
>
<Text style={[
styles.selectorText,
discoverDataSource === DataSource.TMDB && styles.selectorTextActive
]}>TMDB</Text>
</TouchableOpacity>
</View>
)}
/>
</SettingsCard>
<View style={styles.versionContainer}>
<Text style={[styles.versionText, {color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark}]}>
Version 1.0.0
</Text> </Text>
<TouchableOpacity onPress={handleResetSettings} style={styles.resetButton}>
<Text style={[styles.resetButtonText, {color: colors.primary}]}>Reset</Text>
</TouchableOpacity>
</View> </View>
</ScrollView>
</SafeAreaView> {/* Content Container */}
<View style={styles.contentContainer}>
<ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
>
<SettingsCard isDarkMode={isDarkMode} title="User & Account">
<SettingItem
title="Trakt"
description={isAuthenticated ? `Connected as ${userProfile?.username || 'User'}` : "Not Connected"}
icon="person"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('TraktSettings')}
isLast={true}
/>
</SettingsCard>
<SettingsCard isDarkMode={isDarkMode} title="Features">
<SettingItem
title="Calendar"
description="Manage your show calendar settings"
icon="calendar-today"
renderControl={ChevronRight}
onPress={() => navigation.navigate('Calendar')}
isDarkMode={isDarkMode}
/>
<SettingItem
title="Notifications"
description="Configure episode notifications and reminders"
icon="notifications"
renderControl={ChevronRight}
onPress={() => navigation.navigate('NotificationSettings')}
isDarkMode={isDarkMode}
isLast={true}
/>
</SettingsCard>
<SettingsCard isDarkMode={isDarkMode} title="Content">
<SettingItem
title="Addons"
description="Manage your installed addons"
icon="extension"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('Addons')}
badge={addonCount}
/>
<SettingItem
title="Catalogs"
description="Configure content sources"
icon="view-list"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('CatalogSettings')}
badge={catalogCount}
/>
<SettingItem
title="Home Screen"
description="Customize layout and content"
icon="home"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('HomeScreenSettings')}
/>
<SettingItem
title="Ratings Source"
description={mdblistKeySet ? "MDBList API Configured" : "Configure MDBList API"}
icon="info-outline"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('MDBListSettings')}
/>
<SettingItem
title="TMDB"
description="API & Metadata Settings"
icon="movie-filter"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('TMDBSettings')}
isLast={true}
/>
</SettingsCard>
<SettingsCard isDarkMode={isDarkMode} title="Playback">
<SettingItem
title="Video Player"
description={Platform.OS === 'ios'
? (settings.preferredPlayer === 'internal'
? 'Built-in Player'
: settings.preferredPlayer
? settings.preferredPlayer.toUpperCase()
: 'Built-in Player')
: (settings.useExternalPlayer ? 'External Player' : 'Built-in Player')
}
icon="play-arrow"
isDarkMode={isDarkMode}
renderControl={ChevronRight}
onPress={() => navigation.navigate('PlayerSettings')}
isLast={true}
/>
</SettingsCard>
<SettingsCard isDarkMode={isDarkMode} title="Discover">
<SettingItem
title="Content Source"
description="Choose where to get content for the Discover screen"
icon="explore"
isDarkMode={isDarkMode}
renderControl={() => (
<View style={styles.selectorContainer}>
<TouchableOpacity
style={[
styles.selectorButton,
discoverDataSource === DataSource.STREMIO_ADDONS && styles.selectorButtonActive
]}
onPress={() => handleDiscoverDataSourceChange(DataSource.STREMIO_ADDONS)}
>
<Text style={[
styles.selectorText,
discoverDataSource === DataSource.STREMIO_ADDONS && styles.selectorTextActive
]}>Addons</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.selectorButton,
discoverDataSource === DataSource.TMDB && styles.selectorButtonActive
]}
onPress={() => handleDiscoverDataSourceChange(DataSource.TMDB)}
>
<Text style={[
styles.selectorText,
discoverDataSource === DataSource.TMDB && styles.selectorTextActive
]}>TMDB</Text>
</TouchableOpacity>
</View>
)}
/>
</SettingsCard>
<View style={styles.versionContainer}>
<Text style={[styles.versionText, {color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark}]}>
Version 1.0.0
</Text>
</View>
</ScrollView>
</View>
</View>
</View>
); );
}; };
@ -398,34 +433,51 @@ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
}, },
headerBackground: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 1,
},
contentContainer: {
flex: 1,
zIndex: 1,
width: '100%',
},
header: { header: {
paddingHorizontal: 16, paddingHorizontal: 20,
paddingVertical: 12,
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 12 : 8,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'flex-end',
paddingBottom: 8,
backgroundColor: 'transparent',
zIndex: 2,
}, },
headerTitle: { headerTitle: {
fontSize: 32, fontSize: 32,
fontWeight: '700', fontWeight: '800',
letterSpacing: 0.5, letterSpacing: 0.3,
}, },
resetButton: { resetButton: {
paddingVertical: 6, paddingVertical: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
}, },
resetButtonText: { resetButtonText: {
fontSize: 15, fontSize: 16,
fontWeight: '600', fontWeight: '600',
}, },
scrollView: { scrollView: {
flex: 1, flex: 1,
width: '100%',
}, },
scrollContent: { scrollContent: {
flexGrow: 1,
width: '100%',
paddingBottom: 32, paddingBottom: 32,
}, },
cardContainer: { cardContainer: {
width: '100%',
marginBottom: 20, marginBottom: 20,
}, },
cardTitle: { cardTitle: {
@ -444,6 +496,7 @@ const styles = StyleSheet.create({
shadowOpacity: 0.1, shadowOpacity: 0.1,
shadowRadius: 4, shadowRadius: 4,
elevation: 3, elevation: 3,
width: undefined, // Let it fill the container width
}, },
settingItem: { settingItem: {
flexDirection: 'row', flexDirection: 'row',
@ -452,6 +505,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 16, paddingHorizontal: 16,
borderBottomWidth: 0.5, borderBottomWidth: 0.5,
minHeight: 58, minHeight: 58,
width: '100%',
}, },
settingItemBorder: { settingItemBorder: {
// Border styling handled directly in the component with borderBottomWidth // Border styling handled directly in the component with borderBottomWidth