mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
1776 lines
No EOL
59 KiB
TypeScript
1776 lines
No EOL
59 KiB
TypeScript
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';
|
|
import { ScrollToTopProvider, useScrollToTopEmitter } from '../contexts/ScrollToTopContext';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
// 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';
|
|
import DebridIntegrationScreen from '../screens/DebridIntegrationScreen';
|
|
import {
|
|
ContentDiscoverySettingsScreen,
|
|
AppearanceSettingsScreen,
|
|
IntegrationsSettingsScreen,
|
|
PlaybackSettingsScreen,
|
|
AboutSettingsScreen,
|
|
DeveloperSettingsScreen,
|
|
} from '../screens/settings';
|
|
|
|
|
|
// Optional Android immersive mode module
|
|
let RNImmersiveMode: any = null;
|
|
if (Platform.OS === 'android') {
|
|
try {
|
|
RNImmersiveMode = require('react-native-immersive-mode').default;
|
|
} catch {
|
|
RNImmersiveMode = null;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
title?: string;
|
|
episodeId?: string;
|
|
episodeThumbnail?: string;
|
|
fromPlayer?: boolean;
|
|
metadata?: {
|
|
poster?: string;
|
|
banner?: string;
|
|
releaseInfo?: string;
|
|
genres?: string[];
|
|
};
|
|
resumeTime?: number;
|
|
duration?: number;
|
|
addonId?: string;
|
|
};
|
|
PlayerIOS: {
|
|
uri: string;
|
|
title?: string;
|
|
season?: number;
|
|
episode?: number;
|
|
episodeTitle?: string;
|
|
quality?: string;
|
|
year?: number;
|
|
streamProvider?: string;
|
|
streamName?: string;
|
|
headers?: { [key: string]: string };
|
|
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 };
|
|
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;
|
|
DebridIntegration: undefined;
|
|
// New organized settings screens
|
|
ContentDiscoverySettings: undefined;
|
|
AppearanceSettings: undefined;
|
|
IntegrationsSettings: undefined;
|
|
PlaybackSettings: undefined;
|
|
AboutSettings: undefined;
|
|
DeveloperSettings: undefined;
|
|
};
|
|
|
|
|
|
export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
|
|
|
// 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<RootStackParamList>();
|
|
const Tab = createBottomTabNavigator<MainTabParamList>();
|
|
|
|
// 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 (
|
|
<Animated.View style={{
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
transform: [{ scale: scaleAnim }]
|
|
}}>
|
|
{iconLibrary === 'feather' ? (
|
|
<Feather
|
|
name={finalIconName as any}
|
|
size={24}
|
|
color={color}
|
|
/>
|
|
) : iconLibrary === 'ionicons' ? (
|
|
<Ionicons
|
|
name={finalIconName as any}
|
|
size={24}
|
|
color={color}
|
|
/>
|
|
) : (
|
|
<MaterialCommunityIcons
|
|
name={finalIconName as any}
|
|
size={24}
|
|
color={color}
|
|
/>
|
|
)}
|
|
</Animated.View>
|
|
);
|
|
});
|
|
|
|
// 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 (
|
|
<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: isTablet ? (insets.top + 64) : (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
|
|
const MainTabs = () => {
|
|
const { t } = useTranslation();
|
|
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), []);
|
|
const emitScrollToTop = useScrollToTopEmitter();
|
|
// 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 (
|
|
<Animated.View
|
|
style={[{
|
|
position: 'absolute',
|
|
top: insets.top + 12,
|
|
left: 0,
|
|
right: 0,
|
|
alignItems: 'center',
|
|
backgroundColor: 'transparent',
|
|
zIndex: 100,
|
|
}, shouldKeepFixed ? {} : {
|
|
transform: [{ translateY }],
|
|
opacity: fade,
|
|
}]}>
|
|
<View style={{
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
borderRadius: 28,
|
|
overflow: 'hidden',
|
|
padding: 4,
|
|
position: 'relative',
|
|
backgroundColor: isIosTablet ? 'transparent' : 'rgba(0,0,0,0.7)'
|
|
}}>
|
|
{isIosTablet && (
|
|
GlassViewComp && liquidGlassAvailable ? (
|
|
<GlassViewComp
|
|
style={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
borderRadius: 28,
|
|
}}
|
|
glassEffectStyle="clear"
|
|
/>
|
|
) : (
|
|
<BlurView
|
|
tint="dark"
|
|
intensity={75}
|
|
style={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
borderRadius: 28,
|
|
}}
|
|
/>
|
|
)
|
|
)}
|
|
{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) {
|
|
// Same tab pressed - emit scroll to top
|
|
emitScrollToTop(route.name);
|
|
} else if (!event.defaultPrevented) {
|
|
props.navigation.navigate(route.name);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
key={route.key}
|
|
activeOpacity={0.8}
|
|
onPress={onPress}
|
|
style={{
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 10,
|
|
marginHorizontal: 2,
|
|
borderRadius: 24,
|
|
backgroundColor: isFocused ? 'rgba(255,255,255,0.12)' : 'transparent',
|
|
}}
|
|
>
|
|
<Text style={{
|
|
color: isFocused ? currentTheme.colors.primary : currentTheme.colors.white,
|
|
fontWeight: '700',
|
|
fontSize: 14,
|
|
letterSpacing: 0.2,
|
|
}}>
|
|
{typeof label === 'string' ? label : ''}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
);
|
|
})}
|
|
</View>
|
|
</Animated.View>
|
|
);
|
|
}
|
|
|
|
// Default bottom tab for phones
|
|
return (
|
|
<View style={{
|
|
position: 'absolute',
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: Platform.OS === 'android' ? 70 + insets.bottom : 85 + insets.bottom,
|
|
backgroundColor: 'transparent',
|
|
overflow: 'hidden',
|
|
}}>
|
|
{Platform.OS === 'ios' ? (
|
|
GlassViewComp && liquidGlassAvailable ? (
|
|
<GlassViewComp
|
|
style={{
|
|
position: 'absolute',
|
|
height: '100%',
|
|
width: '100%',
|
|
}}
|
|
glassEffectStyle="clear"
|
|
/>
|
|
) : (
|
|
<BlurView
|
|
tint="dark"
|
|
intensity={75}
|
|
style={{
|
|
position: 'absolute',
|
|
height: '100%',
|
|
width: '100%',
|
|
borderTopColor: currentTheme.colors.border,
|
|
borderTopWidth: 0.5,
|
|
shadowColor: currentTheme.colors.black,
|
|
shadowOffset: { width: 0, height: -2 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 3,
|
|
}}
|
|
/>
|
|
)
|
|
) : (
|
|
<LinearGradient
|
|
colors={[
|
|
'rgba(0, 0, 0, 0)',
|
|
'rgba(0, 0, 0, 0.65)',
|
|
'rgba(0, 0, 0, 0.85)',
|
|
'rgba(0, 0, 0, 0.98)',
|
|
]}
|
|
locations={[0, 0.2, 0.4, 0.8]}
|
|
style={{
|
|
position: 'absolute',
|
|
height: '100%',
|
|
width: '100%',
|
|
}}
|
|
/>
|
|
)}
|
|
<View
|
|
style={{
|
|
height: '100%',
|
|
paddingBottom: Platform.OS === 'android' ? 15 + insets.bottom : 20 + insets.bottom,
|
|
paddingTop: Platform.OS === 'android' ? 8 : 12,
|
|
backgroundColor: 'transparent',
|
|
}}
|
|
>
|
|
<View style={{ flexDirection: 'row', paddingTop: 4 }}>
|
|
{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) {
|
|
// Same tab pressed - emit scroll to top
|
|
emitScrollToTop(route.name);
|
|
} else if (!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 (
|
|
<TouchableOpacity
|
|
key={route.key}
|
|
activeOpacity={0.7}
|
|
onPress={onPress}
|
|
style={{
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: 'transparent',
|
|
}}
|
|
>
|
|
<TabIcon
|
|
focused={isFocused}
|
|
color={isFocused ? currentTheme.colors.primary : currentTheme.colors.white}
|
|
iconName={iconName}
|
|
iconLibrary={iconLibrary}
|
|
/>
|
|
<Text
|
|
style={{
|
|
fontSize: 12,
|
|
fontWeight: '600',
|
|
marginTop: 4,
|
|
color: isFocused ? currentTheme.colors.primary : currentTheme.colors.white,
|
|
opacity: isFocused ? 1 : 0.7,
|
|
}}
|
|
>
|
|
{typeof label === 'string' ? label : ''}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
);
|
|
})}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
// 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 (
|
|
<View style={{ flex: 1, backgroundColor: currentTheme.colors.darkBackground }}>
|
|
<StatusBar
|
|
translucent
|
|
barStyle="light-content"
|
|
backgroundColor="transparent"
|
|
/>
|
|
<IOSTab.Navigator
|
|
key={`ios-tabs-${downloadsEnabled ? 'with-dl' : 'no-dl'}`}
|
|
initialRouteName="Home"
|
|
// Native tab bar handles its own visuals; keep options minimal
|
|
screenOptions={{
|
|
headerShown: false,
|
|
tabBarActiveTintColor: currentTheme.colors.primary,
|
|
tabBarInactiveTintColor: currentTheme.colors.white,
|
|
translucent: true,
|
|
// Prefer native lazy/freeze when available; still pass for parity
|
|
lazy: true,
|
|
freezeOnBlur: true,
|
|
}}
|
|
>
|
|
<IOSTab.Screen
|
|
name="Home"
|
|
component={HomeScreen}
|
|
options={{
|
|
title: t('navigation.home'),
|
|
tabBarIcon: () => ({ sfSymbol: 'house' }),
|
|
freezeOnBlur: true,
|
|
}}
|
|
listeners={({ navigation }: { navigation: any }) => ({
|
|
tabPress: (e: any) => {
|
|
if (navigation.isFocused()) {
|
|
emitScrollToTop('Home');
|
|
}
|
|
},
|
|
})}
|
|
/>
|
|
<IOSTab.Screen
|
|
name="Library"
|
|
component={LibraryScreen}
|
|
options={{
|
|
title: t('navigation.library'),
|
|
tabBarIcon: () => ({ sfSymbol: 'heart' }),
|
|
}}
|
|
listeners={({ navigation }: { navigation: any }) => ({
|
|
tabPress: (e: any) => {
|
|
if (navigation.isFocused()) {
|
|
emitScrollToTop('Library');
|
|
}
|
|
},
|
|
})}
|
|
/>
|
|
<IOSTab.Screen
|
|
name="Search"
|
|
component={SearchScreen}
|
|
options={{
|
|
title: t('navigation.search'),
|
|
tabBarIcon: () => ({ sfSymbol: 'magnifyingglass' }),
|
|
}}
|
|
listeners={({ navigation }: { navigation: any }) => ({
|
|
tabPress: (e: any) => {
|
|
if (navigation.isFocused()) {
|
|
emitScrollToTop('Search');
|
|
}
|
|
},
|
|
})}
|
|
/>
|
|
{downloadsEnabled && (
|
|
<IOSTab.Screen
|
|
name="Downloads"
|
|
component={DownloadsScreen}
|
|
options={{
|
|
title: t('navigation.downloads'),
|
|
tabBarIcon: () => ({ sfSymbol: 'arrow.down.circle' }),
|
|
}}
|
|
listeners={({ navigation }: { navigation: any }) => ({
|
|
tabPress: (e: any) => {
|
|
if (navigation.isFocused()) {
|
|
emitScrollToTop('Downloads');
|
|
}
|
|
},
|
|
})}
|
|
/>
|
|
)}
|
|
<IOSTab.Screen
|
|
name="Settings"
|
|
component={SettingsScreen}
|
|
options={{
|
|
title: t('navigation.settings'),
|
|
tabBarIcon: () => ({ sfSymbol: 'gear' }),
|
|
}}
|
|
listeners={({ navigation }: { navigation: any }) => ({
|
|
tabPress: (e: any) => {
|
|
if (navigation.isFocused()) {
|
|
emitScrollToTop('Settings');
|
|
}
|
|
},
|
|
})}
|
|
/>
|
|
</IOSTab.Navigator>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={{ flex: 1, backgroundColor: currentTheme.colors.darkBackground }}>
|
|
{/* Common StatusBar for all tabs */}
|
|
<StatusBar
|
|
translucent
|
|
barStyle="light-content"
|
|
backgroundColor="transparent"
|
|
/>
|
|
|
|
<Tab.Navigator
|
|
tabBar={renderTabBar}
|
|
screenOptions={({ route, navigation, theme }) => ({
|
|
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,
|
|
})}
|
|
>
|
|
<Tab.Screen
|
|
name="Home"
|
|
component={HomeScreen}
|
|
options={{
|
|
tabBarLabel: t('navigation.home'),
|
|
tabBarIcon: ({ color, size, focused }) => (
|
|
<MaterialCommunityIcons name={focused ? 'home' : 'home-outline'} size={size} color={color} />
|
|
),
|
|
freezeOnBlur: true,
|
|
}}
|
|
/>
|
|
<Tab.Screen
|
|
name="Library"
|
|
component={LibraryScreen}
|
|
options={{
|
|
tabBarLabel: t('navigation.library'),
|
|
tabBarIcon: ({ color, size, focused }) => (
|
|
<MaterialCommunityIcons name={focused ? 'heart' : 'heart-outline'} size={size} color={color} />
|
|
),
|
|
}}
|
|
/>
|
|
<Tab.Screen
|
|
name="Search"
|
|
component={SearchScreen}
|
|
options={{
|
|
tabBarLabel: t('navigation.search'),
|
|
tabBarIcon: ({ color, size }) => (
|
|
<MaterialCommunityIcons name={'magnify'} size={size} color={color} />
|
|
),
|
|
}}
|
|
/>
|
|
{appSettings?.enableDownloads !== false && (
|
|
<Tab.Screen
|
|
name="Downloads"
|
|
component={DownloadsScreen}
|
|
options={{
|
|
tabBarLabel: t('navigation.downloads'),
|
|
tabBarIcon: ({ color, size, focused }) => (
|
|
<MaterialCommunityIcons name={focused ? 'download' : 'download-outline'} size={size} color={color} />
|
|
),
|
|
}}
|
|
/>
|
|
)}
|
|
<Tab.Screen
|
|
name="Settings"
|
|
component={SettingsScreen}
|
|
options={{
|
|
tabBarLabel: t('navigation.settings'),
|
|
tabBarIcon: ({ color, size, focused }) => (
|
|
<MaterialCommunityIcons name={focused ? 'cog' : 'cog-outline'} size={size} color={color} />
|
|
),
|
|
}}
|
|
/>
|
|
</Tab.Navigator>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
// 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 system navigation bar is shown by default
|
|
try {
|
|
if (RNImmersiveMode) {
|
|
RNImmersiveMode.setBarMode('Normal');
|
|
RNImmersiveMode.fullLayout(false);
|
|
}
|
|
} catch (error) {
|
|
console.log('Immersive mode error:', error);
|
|
}
|
|
|
|
// Ensure consistent background color for Android
|
|
StatusBar.setBackgroundColor('transparent', true);
|
|
StatusBar.setTranslucent(true);
|
|
}
|
|
}, []);
|
|
|
|
return (
|
|
<SafeAreaProvider>
|
|
<StatusBar
|
|
translucent
|
|
backgroundColor="transparent"
|
|
barStyle="light-content"
|
|
/>
|
|
<PaperProvider theme={CustomDarkTheme}>
|
|
<View style={{
|
|
flex: 1,
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
...(Platform.OS === 'android' && {
|
|
// Prevent white flashes on Android
|
|
opacity: 1,
|
|
})
|
|
}}>
|
|
<Stack.Navigator
|
|
initialRouteName={initialRouteName || 'MainTabs'}
|
|
screenOptions={{
|
|
headerShown: false,
|
|
// Freeze non-focused stack screens to prevent background re-renders (e.g., SeriesContent behind player)
|
|
freezeOnBlur: true,
|
|
// Use slide_from_right for consistency and smooth transitions
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
|
// Ensure consistent background during transitions
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
// Improve Android performance with custom interpolator
|
|
...(Platform.OS === 'android' && {
|
|
cardStyleInterpolator: ({ current, layouts }: any) => {
|
|
return {
|
|
cardStyle: {
|
|
transform: [
|
|
{
|
|
translateX: current.progress.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: [layouts.screen.width, 0],
|
|
}),
|
|
},
|
|
],
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
};
|
|
},
|
|
}),
|
|
}}
|
|
>
|
|
<Stack.Screen
|
|
name="Account"
|
|
component={AuthScreen as any}
|
|
options={{
|
|
headerShown: false,
|
|
animation: 'fade',
|
|
contentStyle: { backgroundColor: currentTheme.colors.darkBackground },
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="Onboarding"
|
|
component={OnboardingScreen}
|
|
options={{
|
|
headerShown: false,
|
|
animation: 'fade',
|
|
animationDuration: 300,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="MainTabs"
|
|
component={MainTabs as any}
|
|
options={{
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="AccountManage"
|
|
component={AccountManageScreen as any}
|
|
options={{
|
|
headerShown: false,
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 200,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="Metadata"
|
|
component={MetadataScreen}
|
|
options={{
|
|
headerShown: false,
|
|
animation: Platform.OS === 'android' ? 'fade' : 'fade',
|
|
animationDuration: Platform.OS === 'android' ? 200 : 300,
|
|
...(Platform.OS === 'ios' && {
|
|
cardStyleInterpolator: customFadeInterpolator,
|
|
animationTypeForReplace: 'push',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
}),
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="Streams"
|
|
component={StreamsScreen as any}
|
|
options={{
|
|
headerShown: false,
|
|
animation: Platform.OS === 'ios' ? 'slide_from_bottom' : 'fade',
|
|
animationDuration: Platform.OS === 'android' ? 200 : 300,
|
|
gestureEnabled: true,
|
|
gestureDirection: Platform.OS === 'ios' ? 'vertical' : 'horizontal',
|
|
...(Platform.OS === 'ios' && { presentation: 'modal' }),
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
// Freeze when blurred to stop timers/network without full unmount
|
|
freezeOnBlur: true,
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="PlayerIOS"
|
|
component={KSPlayerCore as any}
|
|
options={{
|
|
animation: 'default',
|
|
animationDuration: 0,
|
|
// fullScreenModal required for proper video rendering on iOS
|
|
presentation: 'fullScreenModal',
|
|
// Disable gestures during video playback
|
|
gestureEnabled: false,
|
|
// Ensure proper orientation handling
|
|
orientation: 'landscape',
|
|
contentStyle: {
|
|
backgroundColor: '#000000', // Pure black for video player
|
|
},
|
|
// iPad-specific fullscreen options
|
|
statusBarHidden: true,
|
|
statusBarAnimation: 'none',
|
|
// Freeze when blurred to release resources safely
|
|
freezeOnBlur: true,
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="PlayerAndroid"
|
|
component={AndroidVideoPlayer as any}
|
|
options={{
|
|
animation: 'none',
|
|
animationDuration: 0,
|
|
presentation: 'card',
|
|
// Disable gestures during video playback
|
|
gestureEnabled: false,
|
|
// Ensure proper orientation handling
|
|
orientation: 'landscape',
|
|
contentStyle: {
|
|
backgroundColor: '#000000', // Pure black for video player
|
|
},
|
|
// Freeze when blurred to release resources safely
|
|
freezeOnBlur: true,
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="Catalog"
|
|
component={CatalogScreen as any}
|
|
options={{
|
|
animation: 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="Addons"
|
|
component={AddonsScreen as any}
|
|
options={{
|
|
animation: 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="Search"
|
|
component={SearchScreen as any}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'none' : 'fade',
|
|
animationDuration: Platform.OS === 'android' ? 0 : 350,
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="CatalogSettings"
|
|
component={CatalogSettingsScreen as any}
|
|
options={{
|
|
animation: 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="HomeScreenSettings"
|
|
component={HomeScreenSettings}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'default',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 200,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="ContinueWatchingSettings"
|
|
component={ContinueWatchingSettingsScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'default',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 200,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="Contributors"
|
|
component={ContributorsScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'default',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 200,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="HeroCatalogs"
|
|
component={HeroCatalogsScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'default',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 200,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="ShowRatings"
|
|
component={ShowRatingsScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'fade_from_bottom' : 'fade',
|
|
animationDuration: Platform.OS === 'android' ? 200 : 200,
|
|
...(Platform.OS === 'ios' && { presentation: 'modal' }),
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: 'transparent',
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="Calendar"
|
|
component={CalendarScreen as any}
|
|
options={{
|
|
animation: 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="NotificationSettings"
|
|
component={NotificationSettingsScreen as any}
|
|
options={{
|
|
animation: 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="MDBListSettings"
|
|
component={MDBListSettingsScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 200,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="TMDBSettings"
|
|
component={TMDBSettingsScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 200,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="TraktSettings"
|
|
component={TraktSettingsScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 200,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="PlayerSettings"
|
|
component={PlayerSettingsScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 200,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="ThemeSettings"
|
|
component={ThemeScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 200,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="ScraperSettings"
|
|
component={PluginsScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 200,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="CastMovies"
|
|
component={CastMoviesScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 200,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="Update"
|
|
component={UpdateScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="AISettings"
|
|
component={AISettingsScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
|
|
<Stack.Screen
|
|
name="Backup"
|
|
component={BackupScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="AIChat"
|
|
component={AIChatScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'fade' : 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 200 : 300,
|
|
presentation: Platform.OS === 'ios' ? 'fullScreenModal' : 'modal',
|
|
gestureEnabled: true,
|
|
gestureDirection: Platform.OS === 'ios' ? 'horizontal' : 'vertical',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="BackdropGallery"
|
|
component={BackdropGalleryScreen}
|
|
options={{
|
|
animation: 'slide_from_right',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: '#000',
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="DebridIntegration"
|
|
component={DebridIntegrationScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="ContentDiscoverySettings"
|
|
component={ContentDiscoverySettingsScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="AppearanceSettings"
|
|
component={AppearanceSettingsScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="IntegrationsSettings"
|
|
component={IntegrationsSettingsScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="PlaybackSettings"
|
|
component={PlaybackSettingsScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="AboutSettings"
|
|
component={AboutSettingsScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
<Stack.Screen
|
|
name="DeveloperSettings"
|
|
component={DeveloperSettingsScreen}
|
|
options={{
|
|
animation: Platform.OS === 'android' ? 'slide_from_right' : 'slide_from_right',
|
|
animationDuration: Platform.OS === 'android' ? 250 : 300,
|
|
presentation: 'card',
|
|
gestureEnabled: true,
|
|
gestureDirection: 'horizontal',
|
|
headerShown: false,
|
|
contentStyle: {
|
|
backgroundColor: currentTheme.colors.darkBackground,
|
|
},
|
|
}}
|
|
/>
|
|
</Stack.Navigator>
|
|
</View>
|
|
</PaperProvider>
|
|
</SafeAreaProvider>
|
|
);
|
|
};
|
|
|
|
const AppNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootStackParamList }) => (
|
|
<PostHogProvider
|
|
apiKey="phc_sk6THCtV3thEAn6cTaA9kL2cHuKDBnlYiSL40ywdS6C"
|
|
options={{
|
|
host: "https://us.i.posthog.com",
|
|
}}
|
|
>
|
|
<ScrollToTopProvider>
|
|
<LoadingProvider>
|
|
<InnerNavigator initialRouteName={initialRouteName} />
|
|
</LoadingProvider>
|
|
</ScrollToTopProvider>
|
|
</PostHogProvider>
|
|
);
|
|
|
|
export default AppNavigator; |