From bd6c52876eae6e22ffc8692f348dc3e059eec62e Mon Sep 17 00:00:00 2001 From: skearney414 Date: Sun, 11 Jan 2026 22:06:50 +0000 Subject: [PATCH] Update MediaCard.tsx Updates Changes --- src/components/media/ContinueWatchingCard.tsx | 51 +++++++++++++------ src/components/media/MediaCard.tsx | 41 +++++++++++---- src/constants/media.ts | 4 ++ 3 files changed, 71 insertions(+), 25 deletions(-) diff --git a/src/components/media/ContinueWatchingCard.tsx b/src/components/media/ContinueWatchingCard.tsx index afeaad8..ed691b3 100644 --- a/src/components/media/ContinueWatchingCard.tsx +++ b/src/components/media/ContinueWatchingCard.tsx @@ -1,4 +1,5 @@ -import { memo } from 'react'; +import { memo, useRef } from 'react'; +import { Animated, Easing } from 'react-native'; import { Image } from 'expo-image'; import { Box, Text } from '@/theme/theme'; import { useTheme } from '@shopify/restyle'; @@ -11,9 +12,11 @@ import { ProgressBar } from '@/components/basic/ProgressBar'; import { getImageSource } from '@/utils/image'; import { type ContinueWatchingEntry } from '@/hooks/useContinueWatching'; import { formatSeasonEpisodeLabel, formatEpisodeCardTitle } from '@/utils/format'; +import { CARD_ANIMATION_DURATION, CARD_SCALE_FOCUSED, CARD_SCALE_NORMAL } from '@/constants/media'; + +const AnimatedBox = Animated.createAnimatedComponent(Box); interface ContinueWatchingCardProps { - /** The continue watching entry to display */ entry: ContinueWatchingEntry; hideText?: boolean; onPress: () => void; @@ -34,10 +37,19 @@ export const ContinueWatchingCard = memo( testID, }: ContinueWatchingCardProps) => { const theme = useTheme(); + const scaleAnim = useRef(new Animated.Value(1)).current; + + const animateFocus = (focused: boolean) => { + Animated.timing(scaleAnim, { + toValue: focused ? CARD_SCALE_FOCUSED : CARD_SCALE_NORMAL, + duration: CARD_ANIMATION_DURATION, + useNativeDriver: true, + easing: Easing.out(Easing.cubic), + }).start(); + }; const { isUpNext, progressRatio, video, metaName, imageUrl, key } = entry; - // Derive display values const clampedProgress = isUpNext ? 0 : Math.min(1, Math.max(0, progressRatio)); const episodeLabel = formatSeasonEpisodeLabel(video); const title = metaName ?? ''; @@ -48,20 +60,29 @@ export const ContinueWatchingCard = memo( onFocused?.()} hasTVPreferredFocus={hasTVPreferredFocus} - withOutline - testID={testID}> - {({ focusStyle }) => ( - + testID={testID} + onFocusChange={(isFocused) => { + animateFocus(isFocused); + if (isFocused) onFocused?.(); + }}> + {() => ( + + position="relative"> } - {!isUpNext && clampedProgress > 0 && clampedProgress < 1 ? ( + {!isUpNext && clampedProgress > 0 && clampedProgress < 1 && ( - ) : null} + )} {!hideText && ( @@ -95,14 +116,14 @@ export const ContinueWatchingCard = memo( {title} - {subtitle ? ( + {subtitle && ( {subtitle} - ) : null} + )} )} - + )} ); diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index b3d05e2..b9cdad5 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,4 +1,5 @@ -import { memo } from 'react'; +import { memo, useRef } from 'react'; +import { Animated, Easing } from 'react-native'; import { Box, Text } from '@/theme/theme'; import { MetaPreview } from '@/types/stremio'; import { Image } from 'expo-image'; @@ -9,6 +10,9 @@ import { Badge } from '@/components/basic/Badge'; import { NO_POSTER_PORTRAIT } from '@/constants/images'; import { Focusable } from '@/components/basic/Focusable'; import { getImageSource } from '@/utils/image'; +import { CARD_ANIMATION_DURATION, CARD_SCALE_FOCUSED, CARD_SCALE_NORMAL } from '@/constants/media'; + +const AnimatedBox = Animated.createAnimatedComponent(Box); interface MediaCardProps { media: MetaPreview; @@ -29,28 +33,45 @@ export const MediaCard = memo( onFocused, }: MediaCardProps) => { const theme = useTheme(); - + const scaleAnim = useRef(new Animated.Value(1)).current; const posterSource = getImageSource(media.poster || media.background, NO_POSTER_PORTRAIT); + + const animateFocus = (focused: boolean) => { + Animated.timing(scaleAnim, { + toValue: focused ? CARD_SCALE_FOCUSED : CARD_SCALE_NORMAL, + duration: CARD_ANIMATION_DURATION, + useNativeDriver: true, + easing: Easing.out(Easing.cubic), + }).start(); + }; + return ( onPress(media)} - withOutline testID={testID} hasTVPreferredFocus={hasTVPreferredFocus} recyclingKey={media.id} onFocusChange={(isFocused) => { + animateFocus(isFocused); if (isFocused) onFocused?.(); }}> - {({ focusStyle }) => ( - + {() => ( + + position="relative"> - {badgeLabel ? ( + {badgeLabel && ( - ) : null} + )} {media.name} - + )} ); diff --git a/src/constants/media.ts b/src/constants/media.ts index 1485fa6..f805847 100644 --- a/src/constants/media.ts +++ b/src/constants/media.ts @@ -2,3 +2,7 @@ export const MEDIA_DETAILS_HEADER_COVER_HEIGHT = 350; // Continue Watching (Home) export const CONTINUE_WATCHING_PAGE_SIZE = 20; + +export const CARD_SCALE_FOCUSED = 1.05; +export const CARD_SCALE_NORMAL = 1; +export const CARD_ANIMATION_DURATION = 200; // ms