diff --git a/assets/Ripple loading animation.lottie b/assets/Ripple loading animation.lottie new file mode 100644 index 00000000..09a3e025 Binary files /dev/null and b/assets/Ripple loading animation.lottie differ diff --git a/src/components/common/DogLoadingSpinner.tsx b/src/components/common/DogLoadingSpinner.tsx index 0bdf6292..de89a627 100644 --- a/src/components/common/DogLoadingSpinner.tsx +++ b/src/components/common/DogLoadingSpinner.tsx @@ -3,7 +3,7 @@ import { View, Text, StyleSheet, Dimensions } from 'react-native'; import LottieView from 'lottie-react-native'; import { useTheme } from '../../contexts/ThemeContext'; -interface DogLoadingSpinnerProps { +interface LoadingSpinnerProps { text?: string; size?: 'small' | 'medium' | 'large'; style?: any; @@ -11,7 +11,7 @@ interface DogLoadingSpinnerProps { offsetY?: number; // optional vertical offset } -const DogLoadingSpinner: React.FC = ({ +const LoadingSpinner: React.FC = ({ text, size = 'large', style, @@ -47,7 +47,7 @@ const DogLoadingSpinner: React.FC = ({ return ( = ({ + text, + size = 'large', + style, + source, + offsetY = 0, +}) => { + const { currentTheme } = useTheme(); + + const getSizeStyles = () => { + switch (size) { + case 'small': + return { width: 60, height: 60 }; + case 'medium': + return { width: 100, height: 100 }; + case 'large': + default: + return { width: 150, height: 150 }; + } + }; + + const getTextSize = () => { + switch (size) { + case 'small': + return 12; + case 'medium': + return 14; + case 'large': + default: + return 16; + } + }; + + return ( + + + {text && ( + + {text} + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + justifyContent: 'center', + alignItems: 'center', + }, + animation: { + // Size will be set by getSizeStyles() + }, + text: { + marginTop: 16, + textAlign: 'center', + fontWeight: '500', + }, +}); + +export default LoadingSpinner; diff --git a/src/contexts/LoadingContext.tsx b/src/contexts/LoadingContext.tsx new file mode 100644 index 00000000..08913494 --- /dev/null +++ b/src/contexts/LoadingContext.tsx @@ -0,0 +1,31 @@ +import React, { createContext, useContext, useState, ReactNode } from 'react'; + +interface LoadingContextValue { + isHomeLoading: boolean; + setHomeLoading: (loading: boolean) => void; +} + +const LoadingContext = createContext(undefined); + +export const LoadingProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [isHomeLoading, setIsHomeLoading] = useState(true); + + const value: LoadingContextValue = { + isHomeLoading, + setHomeLoading: setIsHomeLoading, + }; + + return ( + + {children} + + ); +}; + +export const useLoading = (): LoadingContextValue => { + const context = useContext(LoadingContext); + if (!context) { + throw new Error('useLoading must be used within a LoadingProvider'); + } + return context; +}; diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index cf9b1553..7a5f09fe 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -44,6 +44,7 @@ import OnboardingScreen from '../screens/OnboardingScreen'; import AuthScreen from '../screens/AuthScreen'; import AccountManageScreen from '../screens/AccountManageScreen'; import { AccountProvider, useAccount } from '../contexts/AccountContext'; +import { LoadingProvider, useLoading } from '../contexts/LoadingContext'; import PluginsScreen from '../screens/PluginsScreen'; import CastMoviesScreen from '../screens/CastMoviesScreen'; @@ -419,6 +420,7 @@ const WrappedScreen: React.FC<{Screen: React.ComponentType}> = ({ Screen }) // Tab Navigator const MainTabs = () => { const { currentTheme } = useTheme(); + const { isHomeLoading } = useLoading(); const isTablet = Dimensions.get('window').width >= 768; const insets = useSafeAreaInsets(); const isIosTablet = Platform.OS === 'ios' && isTablet; @@ -438,6 +440,11 @@ const MainTabs = () => { 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; + } + if (isTablet) { // Top floating, text-only pill nav for tablets return ( @@ -840,17 +847,15 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta }), }} > - {!loading && !user && ( - - )} + - + + + ); diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 52d4820e..2687fa0c 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -48,10 +48,11 @@ import { useSettings, settingsEmitter } from '../hooks/useSettings'; import FeaturedContent from '../components/home/FeaturedContent'; import CatalogSection from '../components/home/CatalogSection'; import { SkeletonFeatured } from '../components/home/SkeletonLoaders'; -import DogLoadingSpinner from '../components/common/DogLoadingSpinner'; +import LoadingSpinner from '../components/common/LoadingSpinner'; import homeStyles, { sharedStyles } from '../styles/homeStyles'; import { useTheme } from '../contexts/ThemeContext'; import type { Theme } from '../contexts/ThemeContext'; +import { useLoading } from '../contexts/LoadingContext'; import * as ScreenOrientation from 'expo-screen-orientation'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -94,7 +95,7 @@ const SkeletonCatalog = React.memo(() => { return ( - + ); @@ -104,6 +105,7 @@ const HomeScreen = () => { const navigation = useNavigation>(); const isDarkMode = useColorScheme() === 'dark'; const { currentTheme } = useTheme(); + const { setHomeLoading } = useLoading(); const continueWatchingRef = useRef(null); const { settings } = useSettings(); const { lastUpdate } = useCatalogContext(); // Add catalog context to listen for addon changes @@ -293,6 +295,11 @@ const HomeScreen = () => { return heroLoading && (catalogsLoading && loadedCatalogCount === 0); }, [showHeroSection, featuredLoading, catalogsLoading, loadedCatalogCount]); + // Update global loading state + useEffect(() => { + setHomeLoading(isLoading); + }, [isLoading, setHomeLoading]); + // React to settings changes useEffect(() => { setShowHeroSection(settings.showHeroSection); @@ -539,7 +546,7 @@ const HomeScreen = () => { translucent /> - + ); @@ -639,7 +646,7 @@ const HomeScreen = () => { - + = 768; @@ -585,10 +585,9 @@ const SearchScreen = () => { {searching ? ( - ) : query.trim().length === 1 ? ( diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 24c2de43..14dc1b6d 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -227,7 +227,7 @@ const SettingsScreen: React.FC = () => { const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext(); const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); - const { user, signOut } = useAccount(); + const { user, signOut, loading: accountLoading } = useAccount(); // Tablet-specific state const [selectedCategory, setSelectedCategory] = useState('account'); @@ -378,7 +378,7 @@ const SettingsScreen: React.FC = () => { case 'account': return ( - {user ? ( + {!accountLoading && user ? ( <> { isTablet={isTablet} /> - ) : ( + ) : !accountLoading && !user ? ( { onPress={() => navigation.navigate('Account')} isTablet={isTablet} /> + ) : ( + )}