From 78583c8e801fd3046d4e7371c8a90fae355d9223 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 26 Apr 2025 15:31:06 +0530 Subject: [PATCH] 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. --- App.tsx | 10 +- src/components/NuvioHeader.tsx | 8 +- src/navigation/AppNavigator.tsx | 309 +++++++++++++++++--------- src/screens/DiscoverScreen.tsx | 171 +++++++++------ src/screens/HomeScreen.tsx | 10 +- src/screens/LibraryScreen.tsx | 152 ++++++++----- src/screens/SettingsScreen.tsx | 378 ++++++++++++++++++-------------- 7 files changed, 637 insertions(+), 401 deletions(-) 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