import React, { useEffect, useRef, useMemo, useState } 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, AppState, Easing, Dimensions } from 'react-native'; import { mmkvStorage } from '../services/mmkvStorage'; import { PaperProvider, MD3DarkTheme, MD3LightTheme, adaptNavigationTheme } from 'react-native-paper'; import type { MD3Theme } from 'react-native-paper'; import type { BottomTabBarProps } from '@react-navigation/bottom-tabs'; import { MaterialCommunityIcons, Feather, Ionicons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { BlurView } from 'expo-blur'; import { colors } from '../styles/colors'; import { HeaderVisibility } from '../contexts/HeaderVisibility'; import { Stream } from '../types/streams'; import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../contexts/ThemeContext'; import { PostHogProvider } from 'posthog-react-native'; // Optional iOS Glass effect (expo-glass-effect) with safe fallback let GlassViewComp: any = null; let liquidGlassAvailable = false; if (Platform.OS === 'ios') { try { // Dynamically require so app still runs if the package isn't installed yet const glass = require('expo-glass-effect'); GlassViewComp = glass.GlassView; liquidGlassAvailable = typeof glass.isLiquidGlassAvailable === 'function' ? glass.isLiquidGlassAvailable() : false; } catch { GlassViewComp = null; liquidGlassAvailable = false; } } // Import screens with their proper types import HomeScreen from '../screens/HomeScreen'; import LibraryScreen from '../screens/LibraryScreen'; import SettingsScreen from '../screens/SettingsScreen'; import DownloadsScreen from '../screens/DownloadsScreen'; import MetadataScreen from '../screens/MetadataScreen'; import KSPlayerCore from '../components/player/KSPlayerCore'; import AndroidVideoPlayer from '../components/player/AndroidVideoPlayer'; import CatalogScreen from '../screens/CatalogScreen'; import AddonsScreen from '../screens/AddonsScreen'; import SearchScreen from '../screens/SearchScreen'; import ShowRatingsScreen from '../screens/ShowRatingsScreen'; import CatalogSettingsScreen from '../screens/CatalogSettingsScreen'; import StreamsScreen from '../screens/StreamsScreen'; import CalendarScreen from '../screens/CalendarScreen'; import NotificationSettingsScreen from '../screens/NotificationSettingsScreen'; import MDBListSettingsScreen from '../screens/MDBListSettingsScreen'; import TMDBSettingsScreen from '../screens/TMDBSettingsScreen'; import HomeScreenSettings from '../screens/HomeScreenSettings'; import HeroCatalogsScreen from '../screens/HeroCatalogsScreen'; import TraktSettingsScreen from '../screens/TraktSettingsScreen'; import PlayerSettingsScreen from '../screens/PlayerSettingsScreen'; import ThemeScreen from '../screens/ThemeScreen'; import OnboardingScreen from '../screens/OnboardingScreen'; import AuthScreen from '../screens/AuthScreen'; import AccountManageScreen from '../screens/AccountManageScreen'; import { useAccount } from '../contexts/AccountContext'; import { LoadingProvider, useLoading } from '../contexts/LoadingContext'; import PluginsScreen from '../screens/PluginsScreen'; import CastMoviesScreen from '../screens/CastMoviesScreen'; import UpdateScreen from '../screens/UpdateScreen'; import AISettingsScreen from '../screens/AISettingsScreen'; import AIChatScreen from '../screens/AIChatScreen'; import BackdropGalleryScreen from '../screens/BackdropGalleryScreen'; import BackupScreen from '../screens/BackupScreen'; import ContinueWatchingSettingsScreen from '../screens/ContinueWatchingSettingsScreen'; import ContributorsScreen from '../screens/ContributorsScreen'; // Stack navigator types export type RootStackParamList = { Onboarding: undefined; MainTabs: undefined; Backup: undefined; Home: undefined; Library: undefined; Settings: undefined; Update: undefined; Search: undefined; Calendar: undefined; Metadata: { id: string; type: string; episodeId?: string; addonId?: string; }; Streams: { id: string; type: string; episodeId?: string; episodeThumbnail?: string; fromPlayer?: boolean; }; PlayerIOS: { uri: string; title?: string; season?: number; episode?: number; episodeTitle?: string; quality?: string; year?: number; streamProvider?: string; streamName?: string; headers?: { [key: string]: string }; forceVlc?: boolean; id?: string; type?: string; episodeId?: string; imdbId?: string; availableStreams?: { [providerId: string]: { streams: any[]; addonName: string } }; backdrop?: string; videoType?: string; groupedEpisodes?: { [seasonNumber: number]: any[] }; }; PlayerAndroid: { uri: string; title?: string; season?: number; episode?: number; episodeTitle?: string; quality?: string; year?: number; streamProvider?: string; streamName?: string; headers?: { [key: string]: string }; forceVlc?: boolean; id?: string; type?: string; episodeId?: string; imdbId?: string; availableStreams?: { [providerId: string]: { streams: any[]; addonName: string } }; backdrop?: string; videoType?: string; groupedEpisodes?: { [seasonNumber: number]: any[] }; }; Catalog: { id: string; type: string; addonId?: string; name?: string; genreFilter?: string }; Credits: { mediaId: string; mediaType: string }; ShowRatings: { showId: number }; Account: undefined; AccountManage: undefined; Payment: undefined; PrivacyPolicy: undefined; About: undefined; Addons: undefined; CatalogSettings: undefined; NotificationSettings: undefined; MDBListSettings: undefined; TMDBSettings: undefined; HomeScreenSettings: undefined; HeroCatalogs: undefined; TraktSettings: undefined; PlayerSettings: undefined; ThemeSettings: undefined; ScraperSettings: undefined; CastMovies: { castMember: { id: number; name: string; profile_path: string | null; character?: string; }; }; AISettings: undefined; AIChat: { contentId: string; contentType: 'movie' | 'series'; episodeId?: string; seasonNumber?: number; episodeNumber?: number; title: string; }; BackdropGallery: { tmdbId: number; type: 'movie' | 'tv'; title: string; }; ContinueWatchingSettings: undefined; Contributors: undefined; }; export type RootStackNavigationProp = NativeStackNavigationProp; // Tab navigator types export type MainTabParamList = { Home: undefined; Library: undefined; Search: undefined; Downloads: undefined; Settings: undefined; }; // Custom fonts that satisfy both theme types const fonts = { regular: { fontFamily: 'sans-serif', fontWeight: '400' as const, }, medium: { fontFamily: 'sans-serif-medium', fontWeight: '500' as const, }, bold: { fontFamily: 'sans-serif', fontWeight: '700' as const, }, heavy: { fontFamily: 'sans-serif', fontWeight: '900' as const, }, // MD3 specific fonts displayLarge: { fontFamily: 'sans-serif', fontWeight: '400' as const, letterSpacing: 0, lineHeight: 64, fontSize: 57, }, displayMedium: { fontFamily: 'sans-serif', fontWeight: '400' as const, letterSpacing: 0, lineHeight: 52, fontSize: 45, }, displaySmall: { fontFamily: 'sans-serif', fontWeight: '400' as const, letterSpacing: 0, lineHeight: 44, fontSize: 36, }, headlineLarge: { fontFamily: 'sans-serif', fontWeight: '400' as const, letterSpacing: 0, lineHeight: 40, fontSize: 32, }, headlineMedium: { fontFamily: 'sans-serif', fontWeight: '400' as const, letterSpacing: 0, lineHeight: 36, fontSize: 28, }, headlineSmall: { fontFamily: 'sans-serif', fontWeight: '400' as const, letterSpacing: 0, lineHeight: 32, fontSize: 24, }, titleLarge: { fontFamily: 'sans-serif', fontWeight: '400' as const, letterSpacing: 0, lineHeight: 28, fontSize: 22, }, titleMedium: { fontFamily: 'sans-serif-medium', fontWeight: '500' as const, letterSpacing: 0.15, lineHeight: 24, fontSize: 16, }, titleSmall: { fontFamily: 'sans-serif-medium', fontWeight: '500' as const, letterSpacing: 0.1, lineHeight: 20, fontSize: 14, }, labelLarge: { fontFamily: 'sans-serif-medium', fontWeight: '500' as const, letterSpacing: 0.1, lineHeight: 20, fontSize: 14, }, labelMedium: { fontFamily: 'sans-serif-medium', fontWeight: '500' as const, letterSpacing: 0.5, lineHeight: 16, fontSize: 12, }, labelSmall: { fontFamily: 'sans-serif-medium', fontWeight: '500' as const, letterSpacing: 0.5, lineHeight: 16, fontSize: 11, }, bodyLarge: { fontFamily: 'sans-serif', fontWeight: '400' as const, letterSpacing: 0.15, lineHeight: 24, fontSize: 16, }, bodyMedium: { fontFamily: 'sans-serif', fontWeight: '400' as const, letterSpacing: 0.25, lineHeight: 20, fontSize: 14, }, bodySmall: { fontFamily: 'sans-serif', fontWeight: '400' as const, letterSpacing: 0.4, lineHeight: 16, fontSize: 12, }, } as const; // Create navigators const Stack = createNativeStackNavigator(); const Tab = createBottomTabNavigator(); // Create custom paper themes export const CustomLightTheme: MD3Theme = { ...MD3LightTheme, colors: { ...MD3LightTheme.colors, primary: colors.primary, }, fonts: MD3LightTheme.fonts, }; export const CustomDarkTheme: MD3Theme = { ...MD3DarkTheme, colors: { ...MD3DarkTheme.colors, primary: colors.primary, }, fonts: MD3DarkTheme.fonts, }; // Create custom navigation theme const { LightTheme, DarkTheme } = adaptNavigationTheme({ reactNavigationLight: NavigationDefaultTheme, reactNavigationDark: NavigationDarkTheme, }); // Add fonts to navigation themes export const CustomNavigationLightTheme: Theme = { ...LightTheme, colors: { ...LightTheme.colors, background: colors.white, card: colors.white, text: colors.textDark, border: colors.border, }, fonts, }; export const CustomNavigationDarkTheme: Theme = { ...DarkTheme, colors: { ...DarkTheme.colors, background: colors.darkBackground, card: colors.darkBackground, text: colors.text, border: colors.border, }, fonts, }; type IconNameType = string; // Add TabIcon component const TabIcon = React.memo(({ focused, color, iconName, iconLibrary = 'material' }: { focused: boolean; color: string; iconName: IconNameType; iconLibrary?: 'material' | 'feather' | 'ionicons'; }) => { const scaleAnim = useRef(new Animated.Value(1)).current; useEffect(() => { Animated.spring(scaleAnim, { toValue: focused ? 1.1 : 1, useNativeDriver: true, friction: 8, tension: 100 }).start(); }, [focused]); // Use outline variant when available for Material icons; Feather has single-form icons const finalIconName = (() => { if (iconLibrary === 'feather') { return iconName; } if (iconName === 'magnify') return 'magnify'; return focused ? iconName : `${iconName}-outline` as IconNameType; })(); return ( {iconLibrary === 'feather' ? ( ) : iconLibrary === 'ionicons' ? ( ) : ( )} ); }); // Update the TabScreenWrapper component with fixed layout dimensions const TabScreenWrapper: React.FC<{children: React.ReactNode}> = ({ children }) => { const [dimensions, setDimensions] = useState(Dimensions.get('window')); useEffect(() => { const subscription = Dimensions.addEventListener('change', ({ window }) => { setDimensions(window); }); return () => subscription?.remove(); }, []); const isTablet = useMemo(() => { const { width, height } = dimensions; const smallestDimension = Math.min(width, height); return (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768); }, [dimensions]); const insets = useSafeAreaInsets(); // 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 = () => { const { currentTheme } = useTheme(); const { settings } = require('../hooks/useSettings'); const { useSettings: useSettingsHook } = require('../hooks/useSettings'); const { settings: appSettings } = useSettingsHook(); const [hasUpdateBadge, setHasUpdateBadge] = React.useState(false); const [dimensions, setDimensions] = useState(Dimensions.get('window')); useEffect(() => { const subscription = Dimensions.addEventListener('change', ({ window }) => { setDimensions(window); }); return () => subscription?.remove(); }, []); React.useEffect(() => { if (Platform.OS !== 'android') return; let mounted = true; const load = async () => { try { const flag = await mmkvStorage.getItem('@update_badge_pending'); if (mounted) setHasUpdateBadge(flag === 'true'); } catch {} }; load(); // Fast poll initially for quick badge appearance, then slow down const fast = setInterval(load, 800); const slowTimer = setTimeout(() => { clearInterval(fast); const slow = setInterval(load, 10000); // store slow interval id on closure for cleanup (load as any)._slow = slow; }, 6000); const onAppStateChange = (state: string) => { if (state === 'active') load(); }; const sub = AppState.addEventListener('change', onAppStateChange); return () => { mounted = false; clearInterval(fast); // @ts-ignore if ((load as any)._slow) clearInterval((load as any)._slow); clearTimeout(slowTimer); sub.remove(); }; }, []); const { isHomeLoading } = useLoading(); const isTablet = useMemo(() => { const { width, height } = dimensions; const smallestDimension = Math.min(width, height); return (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768); }, [dimensions]); const insets = useSafeAreaInsets(); const isIosTablet = Platform.OS === 'ios' && isTablet; const [hidden, setHidden] = React.useState(HeaderVisibility.isHidden()); React.useEffect(() => HeaderVisibility.subscribe(setHidden), []); // Smooth animate header hide/show const headerAnim = React.useRef(new Animated.Value(0)).current; // 0: shown, 1: hidden React.useEffect(() => { Animated.timing(headerAnim, { toValue: hidden ? 1 : 0, duration: 220, easing: Easing.out(Easing.cubic), useNativeDriver: true, }).start(); }, [hidden, headerAnim]); const translateY = headerAnim.interpolate({ inputRange: [0, 1], outputRange: [0, -70] }); const fade = headerAnim.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }); const renderTabBar = (props: BottomTabBarProps) => { // Hide tab bar when home is loading if (isHomeLoading) { return null; } // Get current route name to determine if we should keep navigation fixed const currentRoute = props.state.routes[props.state.index]?.name; const shouldKeepFixed = currentRoute === 'Search' || currentRoute === 'Library'; if (isTablet) { // Top floating, text-only pill nav for tablets return ( {isIosTablet && ( GlassViewComp && liquidGlassAvailable ? ( ) : ( ) )} {props.state.routes.map((route, index) => { const { options } = props.descriptors[route.key]; const label = options.tabBarLabel !== undefined ? options.tabBarLabel : options.title !== undefined ? options.title : route.name; const isFocused = props.state.index === index; const onPress = () => { const event = props.navigation.emit({ type: 'tabPress', target: route.key, canPreventDefault: true, }); if (!isFocused && !event.defaultPrevented) { props.navigation.navigate(route.name); } }; return ( {typeof label === 'string' ? label : ''} ); })} ); } // Default bottom tab for phones return ( {Platform.OS === 'ios' ? ( GlassViewComp && liquidGlassAvailable ? ( ) : ( ) ) : ( )} {props.state.routes.map((route, index) => { const { options } = props.descriptors[route.key]; const label = options.tabBarLabel !== undefined ? options.tabBarLabel : options.title !== undefined ? options.title : route.name; const isFocused = props.state.index === index; const onPress = () => { const event = props.navigation.emit({ type: 'tabPress', target: route.key, canPreventDefault: true, }); if (!isFocused && !event.defaultPrevented) { props.navigation.navigate(route.name); } }; let iconName: IconNameType = 'home'; let iconLibrary: 'material' | 'feather' | 'ionicons' = 'material'; switch (route.name) { case 'Home': iconName = 'home'; iconLibrary = 'feather'; break; case 'Library': iconName = 'library'; iconLibrary = 'ionicons'; break; case 'Search': iconName = 'search'; iconLibrary = 'feather'; break; case 'Downloads': iconName = 'download'; iconLibrary = 'feather'; break; case 'Settings': iconName = 'settings'; iconLibrary = 'feather'; break; } return ( {typeof label === 'string' ? label : ''} ); })} ); }; // iOS: Use native bottom tabs (@bottom-tabs/react-navigation) if (Platform.OS === 'ios') { // Dynamically require to avoid impacting Android bundle const { createNativeBottomTabNavigator } = require('@bottom-tabs/react-navigation'); const IOSTab = createNativeBottomTabNavigator(); const downloadsEnabled = appSettings?.enableDownloads !== false; return ( ({ sfSymbol: 'house' }), freezeOnBlur: true, }} /> ({ sfSymbol: 'heart' }), }} /> ({ sfSymbol: 'magnifyingglass' }), }} /> {downloadsEnabled && ( ({ sfSymbol: 'arrow.down.circle' }), }} /> )} ({ sfSymbol: 'gear' }), }} /> ); } return ( {/* Common StatusBar for all tabs */} ({ transitionSpec: { animation: 'timing', config: { duration: 200, easing: Easing.bezier(0.25, 0.1, 0.25, 1.0), }, }, sceneStyleInterpolator: ({ current }) => ({ sceneStyle: { opacity: current.progress.interpolate({ inputRange: [-1, 0, 1], outputRange: [0, 1, 0], }), transform: [ { scale: current.progress.interpolate({ inputRange: [-1, 0, 1], outputRange: [0.95, 1, 0.95], }), }, { translateY: current.progress.interpolate({ inputRange: [-1, 0, 1], outputRange: [8, 0, 8], }), }, ], }, }), headerShown: false, tabBarShowLabel: false, tabBarStyle: { position: 'absolute', borderTopWidth: 0, elevation: 0, backgroundColor: currentTheme.colors.darkBackground, }, // Ensure background tabs are frozen and detached freezeOnBlur: true, lazy: true, detachInactiveScreens: true, })} > ( ), freezeOnBlur: true, }} /> ( ), }} /> ( ), }} /> {appSettings?.enableDownloads !== false && ( ( ), }} /> )} ( ), }} /> ); }; // Create custom fade animation interpolator for MetadataScreen const customFadeInterpolator = ({ current, layouts }: any) => { return { cardStyle: { opacity: current.progress.interpolate({ inputRange: [0, 1], outputRange: [0, 1], }), transform: [ { scale: current.progress.interpolate({ inputRange: [0, 1], outputRange: [0.95, 1], }), }, ], }, overlayStyle: { opacity: current.progress.interpolate({ inputRange: [0, 1], outputRange: [0, 0.3], }), }, }; }; // Stack Navigator const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootStackParamList }) => { const { currentTheme } = useTheme(); const { user, loading } = useAccount(); const insets = useSafeAreaInsets(); // Handle Android-specific optimizations useEffect(() => { if (Platform.OS === 'android') { // Ensure consistent background color for Android StatusBar.setBackgroundColor('transparent', true); StatusBar.setTranslucent(true); } }, []); return ( { return { cardStyle: { transform: [ { translateX: current.progress.interpolate({ inputRange: [0, 1], outputRange: [layouts.screen.width, 0], }), }, ], backgroundColor: currentTheme.colors.darkBackground, }, }; }, }), }} > ); }; const AppNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootStackParamList }) => ( ); export default AppNavigator;