diff --git a/App.tsx b/App.tsx index c4be52e..d6d1680 100644 --- a/App.tsx +++ b/App.tsx @@ -14,6 +14,7 @@ import { NavigationContainer } from '@react-navigation/native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { StatusBar } from 'expo-status-bar'; import { Provider as PaperProvider } from 'react-native-paper'; +import { enableScreens } from 'react-native-screens'; import AppNavigator, { CustomNavigationDarkTheme, CustomDarkTheme @@ -23,6 +24,9 @@ import { CatalogProvider } from './src/contexts/CatalogContext'; import { GenreProvider } from './src/contexts/GenreContext'; import { TraktProvider } from './src/contexts/TraktContext'; +// This fixes many navigation layout issues by using native screen containers +enableScreens(true); + function App(): React.JSX.Element { // Always use dark mode const isDarkMode = true; @@ -33,7 +37,11 @@ function App(): React.JSX.Element { - + ; export const NuvioHeader = () => { const navigation = useNavigation(); + 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 const isExpoGo = Constants.executionEnvironment === ExecutionEnvironment.StoreClient; diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index b1cda31..896d15c 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -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 { createNativeStackNavigator, NativeStackNavigationOptions, NativeStackNavigationProp } from '@react-navigation/native-stack'; 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 type { MD3Theme } from 'react-native-paper'; import type { BottomTabBarProps } from '@react-navigation/bottom-tabs'; @@ -12,6 +12,7 @@ import { BlurView } from 'expo-blur'; import { colors } from '../styles/colors'; import { NuvioHeader } from '../components/NuvioHeader'; import { Stream } from '../types/streams'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; // Import screens with their proper types 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 ( + + {/* Reserve consistent space for the header area on all screens */} + + {children} + + ); +}; + +// Add this component to wrap each screen in the tab navigator +const WrappedScreen: React.FC<{Screen: React.ComponentType}> = ({ Screen }) => { + return ( + + + + ); +}; + // Tab Navigator const MainTabs = () => { // Always use dark mode @@ -454,112 +514,138 @@ const MainTabs = () => { }; return ( - ({ - tabBarIcon: ({ focused, color, size }) => { - let iconName: IconNameType = 'home'; - - switch (route.name) { - case 'Home': - iconName = 'home'; - break; - case 'Discover': - iconName = 'compass'; - break; - case 'Library': - iconName = 'play-box-multiple'; - break; - case 'Settings': - iconName = 'cog'; - break; - } - - return ; - }, - tabBarActiveTintColor: colors.primary, - tabBarInactiveTintColor: '#FFFFFF', - tabBarStyle: { - position: 'absolute', - backgroundColor: 'transparent', - borderTopWidth: 0, - elevation: 0, - height: 85, - paddingBottom: 20, - paddingTop: 12, - }, - tabBarLabelStyle: { - fontSize: 12, - fontWeight: '600', - marginTop: 0, - }, - tabBarBackground: () => ( - Platform.OS === 'ios' ? ( - - ) : ( - - ) - ), - header: () => route.name === 'Home' ? : null, - headerShown: route.name === 'Home', - })} - > - + {/* Common StatusBar for all tabs */} + - - - - + + ({ + tabBarIcon: ({ focused, color, size }) => { + let iconName: IconNameType = 'home'; + + switch (route.name) { + case 'Home': + iconName = 'home'; + break; + case 'Discover': + iconName = 'compass'; + break; + case 'Library': + iconName = 'play-box-multiple'; + break; + case 'Settings': + iconName = 'cog'; + break; + } + + return ; + }, + tabBarActiveTintColor: colors.primary, + tabBarInactiveTintColor: '#FFFFFF', + tabBarStyle: { + position: 'absolute', + backgroundColor: 'transparent', + borderTopWidth: 0, + elevation: 0, + height: 85, + paddingBottom: 20, + paddingTop: 12, + }, + tabBarLabelStyle: { + fontSize: 12, + fontWeight: '600', + marginTop: 0, + }, + // Completely disable animations between tabs for better performance + animationEnabled: false, + // Keep all screens mounted and active + lazy: false, + freezeOnBlur: false, + detachPreviousScreen: false, + // Configure how the screen renders + detachInactiveScreens: false, + tabBarBackground: () => ( + Platform.OS === 'ios' ? ( + + ) : ( + + ) + ), + header: () => route.name === 'Home' ? : null, + headerShown: route.name === 'Home', + // Add fixed screen styling to help with consistency + contentStyle: { + backgroundColor: colors.darkBackground, + }, + })} + // Global configuration for the tab navigator + detachInactiveScreens={false} + > + + + + + + ); }; @@ -569,7 +655,7 @@ const AppNavigator = () => { const isDarkMode = true; return ( - <> + { { /> - + ); }; diff --git a/src/screens/DiscoverScreen.tsx b/src/screens/DiscoverScreen.tsx index 9672ef2..008d01a 100644 --- a/src/screens/DiscoverScreen.tsx +++ b/src/screens/DiscoverScreen.tsx @@ -24,6 +24,7 @@ import { LinearGradient } from 'expo-linear-gradient'; import { RootStackParamList } from '../navigation/AppNavigator'; import { logger } from '../utils/logger'; import { BlurView } from 'expo-blur'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; interface Category { id: string; @@ -281,10 +282,24 @@ const useStyles = () => { flex: 1, backgroundColor: colors.darkBackground, }, + headerBackground: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + backgroundColor: colors.darkBackground, + zIndex: 1, + }, + contentContainer: { + flex: 1, + backgroundColor: colors.darkBackground, + }, header: { paddingHorizontal: 20, - paddingVertical: 16, - paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 16 : 16, + justifyContent: 'flex-end', + paddingBottom: 8, + backgroundColor: 'transparent', + zIndex: 2, }, headerContent: { flexDirection: 'row', @@ -487,6 +502,24 @@ const DiscoverScreen = () => { const [allContent, setAllContent] = useState([]); const [loading, setLoading] = useState(true); 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 useEffect(() => { @@ -580,17 +613,18 @@ const DiscoverScreen = () => { // Memoize list key extractor 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 ( - - + + {/* Fixed position header background to prevent shifts */} + - {/* Header Section */} - + {/* Header Section with proper top spacing */} + Discover { - {/* Categories Section */} - - - {CATEGORIES.map((category) => ( - handleCategoryPress(category)} - /> - ))} + {/* Rest of the content */} + + {/* Categories Section */} + + + {CATEGORIES.map((category) => ( + handleCategoryPress(category)} + /> + ))} + + + {/* Genres Section */} + + + {COMMON_GENRES.map(genre => ( + handleGenrePress(genre)} + /> + ))} + + + + {/* Content Section */} + {loading ? ( + + + + ) : catalogs.length > 0 ? ( + + ) : ( + + + No content found for {selectedGenre !== 'All' ? selectedGenre : 'these filters'} + + + )} - - {/* Genres Section */} - - - {COMMON_GENRES.map(genre => ( - handleGenrePress(genre)} - /> - ))} - - - - {/* Content Section */} - {loading ? ( - - - - ) : catalogs.length > 0 ? ( - - ) : ( - - - No content found for {selectedGenre !== 'All' ? selectedGenre : 'these filters'} - - - )} - + ); }; diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index b06a785..436eaf2 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -417,8 +417,8 @@ const HomeScreen = () => { useCallback(() => { const statusBarConfig = () => { StatusBar.setBarStyle("light-content"); - StatusBar.setTranslucent(true); - StatusBar.setBackgroundColor('transparent'); + StatusBar.setTranslucent(true); + StatusBar.setBackgroundColor('transparent'); }; statusBarConfig(); @@ -745,9 +745,9 @@ const HomeScreen = () => { {hasContinueWatching && ( - - - + + + )} {catalogs.length > 0 ? ( diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index 93422ed..e8b3581 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -24,6 +24,7 @@ import { catalogService } from '../services/catalogService'; import type { StreamingContent } from '../services/catalogService'; import { RootStackParamList } from '../navigation/AppNavigator'; import { logger } from '../utils/logger'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; // Types interface LibraryItem extends StreamingContent { @@ -97,6 +98,24 @@ const LibraryScreen = () => { const [loading, setLoading] = useState(true); const [libraryItems, setLibraryItems] = useState([]); 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(() => { 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 ( - - + + {/* Fixed position header background to prevent shifts */} + - - - Library + + {/* Header Section with proper top spacing */} + + + Library + + + + {/* Content Container */} + + + {renderFilter('all', 'All', 'apps')} + {renderFilter('movies', 'Movies', 'movie')} + {renderFilter('series', 'TV Shows', 'live-tv')} + + + {loading ? ( + + ) : filteredItems.length === 0 ? ( + + + Your library is empty + + Add content to your library to keep track of what you're watching + + navigation.navigate('Discover')} + activeOpacity={0.7} + > + Explore Content + + + ) : ( + item.id} + numColumns={2} + contentContainerStyle={styles.listContainer} + showsVerticalScrollIndicator={false} + columnWrapperStyle={styles.columnWrapper} + initialNumToRender={6} + maxToRenderPerBatch={6} + windowSize={5} + removeClippedSubviews={Platform.OS === 'android'} + /> + )} - - - {renderFilter('all', 'All', 'apps')} - {renderFilter('movies', 'Movies', 'movie')} - {renderFilter('series', 'TV Shows', 'live-tv')} - - - {loading ? ( - - ) : filteredItems.length === 0 ? ( - - - Your library is empty - - Add content to your library to keep track of what you're watching - - navigation.navigate('Discover')} - activeOpacity={0.7} - > - Explore Content - - - ) : ( - item.id} - numColumns={2} - contentContainerStyle={styles.listContainer} - showsVerticalScrollIndicator={false} - columnWrapperStyle={styles.columnWrapper} - initialNumToRender={6} - maxToRenderPerBatch={6} - windowSize={5} - removeClippedSubviews={Platform.OS === 'android'} - /> - )} - + ); }; @@ -282,10 +308,24 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: colors.darkBackground, }, + headerBackground: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + backgroundColor: colors.darkBackground, + zIndex: 1, + }, + contentContainer: { + flex: 1, + backgroundColor: colors.darkBackground, + }, header: { paddingHorizontal: 20, - paddingVertical: 16, - paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 16 : 16, + justifyContent: 'flex-end', + paddingBottom: 8, + backgroundColor: 'transparent', + zIndex: 2, }, headerContent: { flexDirection: 'row', diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 3b61c4a..47a5757 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -26,6 +26,7 @@ import { stremioService } from '../services/stremioService'; import { useCatalogContext } from '../contexts/CatalogContext'; import { useTraktContext } from '../contexts/TraktContext'; import { catalogService, DataSource } from '../services/catalogService'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; const { width } = Dimensions.get('window'); @@ -125,6 +126,7 @@ const SettingsScreen: React.FC = () => { const navigation = useNavigation>(); const { lastUpdate } = useCatalogContext(); const { isAuthenticated, userProfile } = useTraktContext(); + const insets = useSafeAreaInsets(); // States for dynamic content const [addonCount, setAddonCount] = useState(0); @@ -132,6 +134,23 @@ const SettingsScreen: React.FC = () => { const [mdblistKeySet, setMdblistKeySet] = useState(false); const [discoverDataSource, setDiscoverDataSource] = useState(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 () => { try { // Load addon count and get their catalogs @@ -231,166 +250,182 @@ const SettingsScreen: React.FC = () => { 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 ( - - - - - Settings - - - Reset - - - - - navigation.navigate('TraktSettings')} - isLast={true} - /> - - - - navigation.navigate('Calendar')} - isDarkMode={isDarkMode} - /> - navigation.navigate('NotificationSettings')} - isDarkMode={isDarkMode} - isLast={true} - /> - - - - navigation.navigate('Addons')} - badge={addonCount} - /> - navigation.navigate('CatalogSettings')} - badge={catalogCount} - /> - navigation.navigate('HomeScreenSettings')} - /> - navigation.navigate('MDBListSettings')} - /> - navigation.navigate('TMDBSettings')} - isLast={true} - /> - - - - navigation.navigate('PlayerSettings')} - isLast={true} - /> - - - - ( - - handleDiscoverDataSourceChange(DataSource.STREMIO_ADDONS)} - > - Addons - - handleDiscoverDataSourceChange(DataSource.TMDB)} - > - TMDB - - - )} - /> - - - - - Version 1.0.0 + {/* Fixed position header background to prevent shifts */} + + + + {/* Header Section with proper top spacing */} + + + Settings + + Reset + - - + + {/* Content Container */} + + + + navigation.navigate('TraktSettings')} + isLast={true} + /> + + + + navigation.navigate('Calendar')} + isDarkMode={isDarkMode} + /> + navigation.navigate('NotificationSettings')} + isDarkMode={isDarkMode} + isLast={true} + /> + + + + navigation.navigate('Addons')} + badge={addonCount} + /> + navigation.navigate('CatalogSettings')} + badge={catalogCount} + /> + navigation.navigate('HomeScreenSettings')} + /> + navigation.navigate('MDBListSettings')} + /> + navigation.navigate('TMDBSettings')} + isLast={true} + /> + + + + navigation.navigate('PlayerSettings')} + isLast={true} + /> + + + + ( + + handleDiscoverDataSourceChange(DataSource.STREMIO_ADDONS)} + > + Addons + + handleDiscoverDataSourceChange(DataSource.TMDB)} + > + TMDB + + + )} + /> + + + + + Version 1.0.0 + + + + + + ); }; @@ -398,34 +433,51 @@ const styles = StyleSheet.create({ container: { flex: 1, }, + headerBackground: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + zIndex: 1, + }, + contentContainer: { + flex: 1, + zIndex: 1, + width: '100%', + }, header: { - paddingHorizontal: 16, - paddingVertical: 12, - paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 12 : 8, + paddingHorizontal: 20, flexDirection: 'row', justifyContent: 'space-between', - alignItems: 'center', + alignItems: 'flex-end', + paddingBottom: 8, + backgroundColor: 'transparent', + zIndex: 2, }, headerTitle: { fontSize: 32, - fontWeight: '700', - letterSpacing: 0.5, + fontWeight: '800', + letterSpacing: 0.3, }, resetButton: { - paddingVertical: 6, + paddingVertical: 8, paddingHorizontal: 12, }, resetButtonText: { - fontSize: 15, + fontSize: 16, fontWeight: '600', }, scrollView: { flex: 1, + width: '100%', }, scrollContent: { + flexGrow: 1, + width: '100%', paddingBottom: 32, }, cardContainer: { + width: '100%', marginBottom: 20, }, cardTitle: { @@ -444,6 +496,7 @@ const styles = StyleSheet.create({ shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, + width: undefined, // Let it fill the container width }, settingItem: { flexDirection: 'row', @@ -452,6 +505,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, borderBottomWidth: 0.5, minHeight: 58, + width: '100%', }, settingItemBorder: { // Border styling handled directly in the component with borderBottomWidth