diff --git a/src/components/home/AppleTVHero.tsx b/src/components/home/AppleTVHero.tsx index b74f6396..65cc6f1d 100644 --- a/src/components/home/AppleTVHero.tsx +++ b/src/components/home/AppleTVHero.tsx @@ -57,15 +57,62 @@ const STATUS_BAR_HEIGHT = StatusBar.currentHeight || 0; const HERO_HEIGHT = height * 0.85; // Animated Pagination Dot Component -const PaginationDot: React.FC<{ isActive: boolean; onPress: () => void }> = React.memo( - ({ isActive, onPress }) => { +const PaginationDot: React.FC<{ + isActive: boolean; + isNext: boolean; + dragProgress: SharedValue; + onPress: () => void; +}> = React.memo( + ({ isActive, isNext, dragProgress, onPress }) => { const animatedStyle = useAnimatedStyle(() => { + // Base values + const activeWidth = 32; + const inactiveWidth = 8; + const activeOpacity = 0.9; + const inactiveOpacity = 0.3; + + // Calculate target width and opacity based on state + let targetWidth = isActive ? activeWidth : inactiveWidth; + let targetOpacity = isActive ? activeOpacity : inactiveOpacity; + + // If this is the next dot during drag, interpolate between inactive and active + if (isNext && dragProgress.value > 0) { + targetWidth = interpolate( + dragProgress.value, + [0, 1], + [inactiveWidth, activeWidth], + Extrapolation.CLAMP + ); + targetOpacity = interpolate( + dragProgress.value, + [0, 1], + [inactiveOpacity, activeOpacity], + Extrapolation.CLAMP + ); + } + + // If this is the current active dot during drag, interpolate from active to inactive + if (isActive && dragProgress.value > 0) { + targetWidth = interpolate( + dragProgress.value, + [0, 1], + [activeWidth, inactiveWidth], + Extrapolation.CLAMP + ); + targetOpacity = interpolate( + dragProgress.value, + [0, 1], + [activeOpacity, inactiveOpacity], + Extrapolation.CLAMP + ); + } + return { - width: withTiming(isActive ? 32 : 8, { + width: withTiming(targetWidth, { duration: 300, easing: Easing.out(Easing.cubic), }), - opacity: withTiming(isActive ? 0.9 : 0.3, { + opacity: withTiming(targetOpacity, { duration: 300, easing: Easing.out(Easing.cubic), }), @@ -890,6 +937,8 @@ const AppleTVHero: React.FC = ({ handleDotPress(index)} /> ))} diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 403b39fc..2a9dd866 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -55,9 +55,9 @@ import { logger } from '../../utils/logger'; import { TMDBService } from '../../services/tmdbService'; import TrailerService from '../../services/trailerService'; import TrailerPlayer from '../video/TrailerPlayer'; +import { HERO_HEIGHT, SCREEN_WIDTH as width, IS_TABLET as isTablet } from '../../constants/dimensions'; -const { width, height } = Dimensions.get('window'); -const isTablet = width >= 768; +const { height } = Dimensions.get('window'); // Ultra-optimized animation constants const SCALE_FACTOR = 1.02; @@ -1540,7 +1540,7 @@ const HeroSection: React.FC = memo(({ buttonsTranslateY.value = 0; logoOpacity.value = 1; heroOpacity.value = 1; - heroHeight.value = height * 0.6; + heroHeight.value = HERO_HEIGHT; } catch (error) { logger.error('HeroSection', 'Error cleaning up animation values:', error); } diff --git a/src/constants/dimensions.ts b/src/constants/dimensions.ts new file mode 100644 index 00000000..f515c391 --- /dev/null +++ b/src/constants/dimensions.ts @@ -0,0 +1,13 @@ +import { Dimensions } from 'react-native'; + +const { width, height } = Dimensions.get('window'); + +// Hero section height - 85% of screen height (matching Apple TV style) +export const HERO_HEIGHT = height * 0.70; + +// Screen dimensions +export const SCREEN_WIDTH = width; +export const SCREEN_HEIGHT = height; + +// Tablet detection +export const IS_TABLET = width >= 768; diff --git a/src/hooks/useMetadataAnimations.ts b/src/hooks/useMetadataAnimations.ts index ab3f55f8..d9c3b803 100644 --- a/src/hooks/useMetadataAnimations.ts +++ b/src/hooks/useMetadataAnimations.ts @@ -9,6 +9,7 @@ import { runOnUI, cancelAnimation, } from 'react-native-reanimated'; +import { HERO_HEIGHT } from '../constants/dimensions'; const { width, height } = Dimensions.get('window'); @@ -40,7 +41,7 @@ export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) = // Combined hero animations const heroOpacity = useSharedValue(1); const heroScale = useSharedValue(1); // Start at 1 for Android compatibility - const heroHeightValue = useSharedValue(height * 0.55); + const heroHeightValue = useSharedValue(HERO_HEIGHT); // Combined UI element animations const uiElementsOpacity = useSharedValue(1); @@ -154,8 +155,8 @@ export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) = const rawScrollY = event.contentOffset.y; scrollY.value = rawScrollY; - // Single calculation for header threshold - const threshold = height * 0.4 - safeAreaTop; + // Single calculation for header threshold - show only when hero is fully scrolled + const threshold = HERO_HEIGHT - safeAreaTop; const progress = rawScrollY > threshold ? 1 : 0; // Use single progress value for all header animations