ui changes on new herocarousal

This commit is contained in:
tapframe 2025-09-03 13:46:20 +05:30
parent de36ec8186
commit 8700b10843
3 changed files with 192 additions and 21 deletions

View file

@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import React, { useMemo, useState } from 'react';
import { View, Text, StyleSheet, Dimensions, TouchableOpacity, ViewStyle, TextStyle, ImageStyle, FlatList, StyleProp } from 'react-native';
import Animated, { FadeIn, Easing } from 'react-native-reanimated';
import Animated, { FadeIn, FadeOut, Easing } from 'react-native-reanimated';
import { LinearGradient } from 'expo-linear-gradient';
import { Image as ExpoImage } from 'expo-image';
import { MaterialIcons } from '@expo/vector-icons';
@ -9,29 +9,115 @@ import { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../../navigation/AppNavigator';
import { StreamingContent } from '../../services/catalogService';
import { useTheme } from '../../contexts/ThemeContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
interface HeroCarouselProps {
items: StreamingContent[];
loading?: boolean;
}
const { width } = Dimensions.get('window');
const CARD_WIDTH = Math.min(width * 0.88, 520);
const CARD_HEIGHT = Math.round(CARD_WIDTH * 9 / 16) + 160; // increased extra space for text/actions
const CARD_WIDTH = Math.min(width * 0.8, 480);
const CARD_HEIGHT = Math.round(CARD_WIDTH * 9 / 16) + 270; // further increased space for text/actions
const HeroCarousel: React.FC<HeroCarouselProps> = ({ items }) => {
const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
const data = useMemo(() => (items && items.length ? items.slice(0, 10) : []), [items]);
const [activeIndex, setActiveIndex] = useState(0);
if (data.length === 0) {
return null;
if (loading) {
return (
<View style={[styles.container, { paddingVertical: 12 }] as StyleProp<ViewStyle>}>
<View style={{ height: CARD_HEIGHT }}>
<FlatList
data={[1, 2, 3] as any}
keyExtractor={(i) => String(i)}
horizontal
showsHorizontalScrollIndicator={false}
snapToInterval={CARD_WIDTH + 16}
decelerationRate="fast"
contentContainerStyle={{ paddingHorizontal: (width - CARD_WIDTH) / 2 }}
renderItem={() => (
<View style={{ width: CARD_WIDTH + 16 }}>
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1 }] as StyleProp<ViewStyle>}>
<View style={styles.bannerContainer as ViewStyle}>
<View style={styles.skeletonBannerFull as ViewStyle} />
<LinearGradient
colors={["transparent", "rgba(0,0,0,0.25)"]}
locations={[0.6, 1]}
style={styles.bannerOverlay as ViewStyle}
/>
</View>
<View style={styles.info as ViewStyle}>
<View style={[styles.skeletonLine, { width: '62%' }] as StyleProp<ViewStyle>} />
<View style={[styles.skeletonLine, { width: '44%', marginTop: 6 }] as StyleProp<ViewStyle>} />
<View style={styles.skeletonActions as ViewStyle}>
<View style={[styles.skeletonPill, { width: 96 }] as StyleProp<ViewStyle>} />
<View style={[styles.skeletonPill, { width: 80 }] as StyleProp<ViewStyle>} />
</View>
</View>
</View>
</View>
)}
/>
</View>
</View>
);
}
if (data.length === 0) return null;
const handleMomentumEnd = (event: any) => {
const offsetX = event?.nativeEvent?.contentOffset?.x ?? 0;
const interval = CARD_WIDTH + 16;
const idx = Math.round(offsetX / interval);
const clamped = Math.max(0, Math.min(idx, data.length - 1));
if (clamped !== activeIndex) setActiveIndex(clamped);
};
return (
<Animated.View entering={FadeIn.duration(350).easing(Easing.out(Easing.cubic))}>
<View style={styles.container as ViewStyle}>
{data[activeIndex] && (
<View
style={[
styles.backgroundContainer,
{ top: -insets.top },
] as StyleProp<ViewStyle>}
pointerEvents="none"
>
<Animated.View
key={data[activeIndex].id}
entering={FadeIn.duration(300).easing(Easing.out(Easing.cubic))}
exiting={FadeOut.duration(300).easing(Easing.in(Easing.cubic))}
style={{ flex: 1 } as ViewStyle}
>
<ExpoImage
source={{ uri: data[activeIndex].banner || data[activeIndex].poster }}
style={styles.backgroundImage as ImageStyle}
contentFit="cover"
blurRadius={36}
cachePolicy="memory-disk"
/>
<LinearGradient
colors={["rgba(0,0,0,0.65)", "rgba(0,0,0,0.85)"]}
locations={[0.4, 1]}
style={styles.backgroundOverlay as ViewStyle}
/>
</Animated.View>
</View>
)}
{/* Bottom blend to HomeScreen background (not the card) */}
<LinearGradient
colors={["transparent", currentTheme.colors.background]}
locations={[0, 1]}
style={styles.bottomBlend as ViewStyle}
pointerEvents="none"
/>
<FlatList
data={data}
keyExtractor={(item) => item.id}
@ -40,6 +126,7 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items }) => {
snapToInterval={CARD_WIDTH + 16}
decelerationRate="fast"
contentContainerStyle={{ paddingHorizontal: (width - CARD_WIDTH) / 2 }}
onMomentumScrollEnd={handleMomentumEnd}
renderItem={({ item }) => (
<View style={{ width: CARD_WIDTH + 16 }}>
<TouchableOpacity
@ -56,8 +143,8 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items }) => {
cachePolicy="memory-disk"
/>
<LinearGradient
colors={["transparent", "rgba(0,0,0,0.7)"]}
locations={[0.55, 1]}
colors={["transparent", "rgba(0,0,0,0.35)"]}
locations={[0.6, 1]}
style={styles.bannerGradient as ViewStyle}
/>
</View>
@ -103,6 +190,34 @@ const styles = StyleSheet.create({
container: {
paddingVertical: 12,
},
backgroundContainer: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
backgroundImage: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
backgroundOverlay: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
bottomBlend: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
height: 160,
},
card: {
width: CARD_WIDTH,
height: CARD_HEIGHT,
@ -114,6 +229,47 @@ const styles = StyleSheet.create({
shadowOpacity: 0.3,
shadowRadius: 12,
},
skeletonCard: {
width: CARD_WIDTH,
height: CARD_HEIGHT,
borderRadius: 16,
overflow: 'hidden',
},
skeletonBannerFull: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
backgroundColor: 'rgba(255,255,255,0.06)'
},
bannerOverlay: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
skeletonInfo: {
paddingHorizontal: 16,
paddingTop: 10,
},
skeletonLine: {
height: 14,
borderRadius: 7,
backgroundColor: 'rgba(255,255,255,0.08)'
},
skeletonActions: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
marginTop: 12,
},
skeletonPill: {
height: 36,
borderRadius: 18,
backgroundColor: 'rgba(255,255,255,0.1)'
},
bannerContainer: {
position: 'absolute',
left: 0,
@ -132,6 +288,7 @@ const styles = StyleSheet.create({
bottom: 0,
top: 0,
},
info: {
position: 'absolute',
left: 0,

View file

@ -312,18 +312,31 @@ export function useFeaturedContent() {
// Subscribe directly to settings emitter for immediate updates
useEffect(() => {
const handleSettingsChange = () => {
// Only refresh if current content source is different from settings
// This prevents duplicate refreshes when HomeScreen also handles this event
if (contentSource !== settings.featuredContentSource) {
logger.info('[useFeaturedContent] event:content-source-changed', { from: contentSource, to: settings.featuredContentSource });
// Content source will be updated in the next render cycle due to state updates
// No need to call loadFeaturedContent here as it will be triggered by contentSource change
} else if (
contentSource === 'catalogs' &&
JSON.stringify(selectedCatalogs) !== JSON.stringify(settings.selectedHeroCatalogs)
) {
// Only refresh if using catalogs and selected catalogs changed
logger.info('[useFeaturedContent] event:selected-catalogs-changed');
// Always reflect settings immediately in this hook
const nextSource = settings.featuredContentSource;
const nextSelected = settings.selectedHeroCatalogs || [];
const sourceChanged = contentSource !== nextSource;
const catalogsChanged = JSON.stringify(selectedCatalogs) !== JSON.stringify(nextSelected);
if (sourceChanged || (nextSource === 'catalogs' && catalogsChanged)) {
logger.info('[useFeaturedContent] event:settings-changed:immediate-refresh', {
fromSource: contentSource,
toSource: nextSource,
catalogsChanged
});
// Update internal state immediately so dependent effects are in sync
setContentSource(nextSource);
setSelectedCatalogs(nextSelected);
// Clear current data to reflect change instantly in UI
setAllFeaturedContent([]);
setFeaturedContent(null);
persistentStore.allFeaturedContent = [];
persistentStore.featuredContent = null;
// Force a fresh load
loadFeaturedContent(true);
}
};

View file

@ -612,6 +612,7 @@ const HomeScreen = () => {
<HeroCarousel
key={`carousel-${featuredContentSource}`}
items={allFeaturedContent || (featuredContent ? [featuredContent] : [])}
loading={featuredLoading}
/>
) : (
<FeaturedContent