From d553be3ec5dfac41d4b1ffd4bd19f78e448b7b28 Mon Sep 17 00:00:00 2001 From: tapframe Date: Wed, 27 Aug 2025 20:03:56 +0530 Subject: [PATCH] improved tablet alyout --- src/components/home/FeaturedContent.tsx | 11 ++- src/contexts/HeaderVisibility.ts | 28 ++++++ src/navigation/AppNavigator.tsx | 117 +++++++++++++++++++++++- src/screens/HomeScreen.tsx | 25 +++++ 4 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 src/contexts/HeaderVisibility.ts diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx index 2cc04c2..54ceec1 100644 --- a/src/components/home/FeaturedContent.tsx +++ b/src/components/home/FeaturedContent.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useMemo } from 'react'; import { View, Text, @@ -176,6 +176,13 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin opacity: overlayOpacity.value, })); + // Stable hero height for tablets to prevent layout jumps; keep hooks unconditional + const tabletHeroHeight = useMemo(() => { + const aspectBased = width * 0.56; // ~16:9 visual + const screenBased = height * 0.62; + return Math.min(screenBased, aspectBased); + }, [width, height, featuredContent?.id]); + // Preload the image const preloadImage = async (url: string): Promise => { // Skip if already cached to prevent redundant prefetch @@ -472,7 +479,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin return ( void; + +let currentHidden = false; +const listeners: Listener[] = []; + +export const HeaderVisibility = { + setHidden(hidden: boolean) { + if (currentHidden === hidden) return; + currentHidden = hidden; + listeners.slice().forEach(l => { + try { l(currentHidden); } catch {} + }); + }, + subscribe(listener: Listener) { + listeners.push(listener); + // Immediate call to sync initial state + try { listener(currentHidden); } catch {} + return () => { + const idx = listeners.indexOf(listener); + if (idx >= 0) listeners.splice(idx, 1); + }; + }, + isHidden() { + return currentHidden; + } +}; + + diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 5d39c45..1088d8f 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, AppState, Easing } from 'react-native'; +import { useColorScheme, Platform, Animated, StatusBar, TouchableOpacity, View, Text, AppState, Easing, Dimensions } 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'; @@ -10,8 +10,9 @@ import { MaterialCommunityIcons } 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 } from 'react-native-safe-area-context'; +import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../contexts/ThemeContext'; import { Toasts } from '@backpackapp-io/react-native-toast'; import { PostHogProvider } from 'posthog-react-native'; @@ -356,6 +357,8 @@ const TabIcon = React.memo(({ focused, color, iconName }: { // Update the TabScreenWrapper component with fixed layout dimensions const TabScreenWrapper: React.FC<{children: React.ReactNode}> = ({ children }) => { + const isTablet = Dimensions.get('window').width >= 768; + const insets = useSafeAreaInsets(); // Force consistent status bar settings useEffect(() => { const applyStatusBarConfig = () => { @@ -390,7 +393,7 @@ const TabScreenWrapper: React.FC<{children: React.ReactNode}> = ({ children }) = }}> {/* Reserve consistent space for the header area on all screens */} }> = ({ Screen }) // Tab Navigator const MainTabs = () => { const { currentTheme } = useTheme(); + const isTablet = Dimensions.get('window').width >= 768; + 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) => { + if (isTablet) { + // Top floating, text-only pill nav for tablets + return ( + + + {isIosTablet && ( + + )} + {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 ( { const memoizedThisWeekSection = useMemo(() => , []); const memoizedContinueWatchingSection = useMemo(() => , []); + // Track scroll direction manually for reliable behavior across platforms + const lastScrollYRef = useRef(0); + const lastToggleRef = useRef(0); + const toggleHeader = useCallback((hide: boolean) => { + const now = Date.now(); + if (now - lastToggleRef.current < 120) return; // debounce + lastToggleRef.current = now; + HeaderVisibility.setHidden(hide); + }, []); const renderListItem = useCallback(({ item, index }: { item: HomeScreenListItem, index: number }) => { const wrapper = (child: React.ReactNode) => ( @@ -724,6 +734,21 @@ const HomeScreen = () => { onEndReached={handleLoadMoreCatalogs} onEndReachedThreshold={0.6} scrollEventThrottle={32} + onScroll={event => { + const y = event.nativeEvent.contentOffset.y; + const dy = y - lastScrollYRef.current; + lastScrollYRef.current = y; + if (y <= 10) { + toggleHeader(false); + return; + } + // Threshold to avoid jitter + if (dy > 6) { + toggleHeader(true); // scrolling down + } else if (dy < -6) { + toggleHeader(false); // scrolling up + } + }} /> {/* Toasts are rendered globally at root */}