Enhance StreamsScreen animations and layout for improved user experience

This update introduces staggered animations for stream cards and provider filters, enhancing the visual appeal during loading states. The layout has been refined to ensure a smoother transition and better organization of stream details and actions. Additionally, the use of Animated.View components improves performance and responsiveness, contributing to a more engaging user interface.
This commit is contained in:
tapframe 2025-05-27 22:19:08 +05:30
parent 10aa799626
commit 92704f0998

View file

@ -42,7 +42,8 @@ import Animated, {
Extrapolate, Extrapolate,
runOnJS, runOnJS,
cancelAnimation, cancelAnimation,
SharedValue SharedValue,
Layout
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
@ -76,78 +77,86 @@ const StreamCard = ({ stream, onPress, index, isLoading, statusMessage, theme }:
const displayTitle = isHDRezka ? `HDRezka ${stream.title}` : (stream.name || stream.title || 'Unnamed Stream'); const displayTitle = isHDRezka ? `HDRezka ${stream.title}` : (stream.name || stream.title || 'Unnamed Stream');
const displayAddonName = isHDRezka ? '' : (stream.title || ''); const displayAddonName = isHDRezka ? '' : (stream.title || '');
// Animation delay based on index - stagger effect
const enterDelay = 100 + (index * 50);
return ( return (
<TouchableOpacity <Animated.View
style={[ entering={FadeInDown.duration(300).delay(enterDelay).springify()}
styles.streamCard, layout={Layout.springify()}
isLoading && styles.streamCardLoading
]}
onPress={onPress}
disabled={isLoading}
activeOpacity={0.7}
> >
<View style={styles.streamDetails}> <TouchableOpacity
<View style={styles.streamNameRow}> style={[
<View style={styles.streamTitleContainer}> styles.streamCard,
<Text style={[styles.streamName, { color: theme.colors.highEmphasis }]}> isLoading && styles.streamCardLoading
{displayTitle} ]}
</Text> onPress={onPress}
{displayAddonName && displayAddonName !== displayTitle && ( disabled={isLoading}
<Text style={[styles.streamAddonName, { color: theme.colors.mediumEmphasis }]}> activeOpacity={0.7}
{displayAddonName} >
<View style={styles.streamDetails}>
<View style={styles.streamNameRow}>
<View style={styles.streamTitleContainer}>
<Text style={[styles.streamName, { color: theme.colors.highEmphasis }]}>
{displayTitle}
</Text> </Text>
{displayAddonName && displayAddonName !== displayTitle && (
<Text style={[styles.streamAddonName, { color: theme.colors.mediumEmphasis }]}>
{displayAddonName}
</Text>
)}
</View>
{/* Show loading indicator if stream is loading */}
{isLoading && (
<View style={styles.loadingIndicator}>
<ActivityIndicator size="small" color={theme.colors.primary} />
<Text style={[styles.loadingText, { color: theme.colors.primary }]}>
{statusMessage || "Loading..."}
</Text>
</View>
)} )}
</View> </View>
{/* Show loading indicator if stream is loading */} <View style={styles.streamMetaRow}>
{isLoading && ( {quality && quality >= "720" && (
<View style={styles.loadingIndicator}> <QualityBadge type="HD" />
<ActivityIndicator size="small" color={theme.colors.primary} /> )}
<Text style={[styles.loadingText, { color: theme.colors.primary }]}>
{statusMessage || "Loading..."} {isDolby && (
</Text> <QualityBadge type="VISION" />
</View> )}
)}
{size && (
<View style={[styles.chip, { backgroundColor: theme.colors.darkGray }]}>
<Text style={[styles.chipText, { color: theme.colors.white }]}>{size}</Text>
</View>
)}
{isDebrid && (
<View style={[styles.chip, { backgroundColor: theme.colors.success }]}>
<Text style={[styles.chipText, { color: theme.colors.white }]}>DEBRID</Text>
</View>
)}
{/* Special badge for HDRezka streams */}
{isHDRezka && (
<View style={[styles.chip, { backgroundColor: theme.colors.accent }]}>
<Text style={[styles.chipText, { color: theme.colors.white }]}>HDREZKA</Text>
</View>
)}
</View>
</View> </View>
<View style={styles.streamMetaRow}> <View style={styles.streamAction}>
{quality && quality >= "720" && ( <MaterialIcons
<QualityBadge type="HD" /> name="play-arrow"
)} size={24}
color={theme.colors.primary}
{isDolby && ( />
<QualityBadge type="VISION" />
)}
{size && (
<View style={[styles.chip, { backgroundColor: theme.colors.darkGray }]}>
<Text style={[styles.chipText, { color: theme.colors.white }]}>{size}</Text>
</View>
)}
{isDebrid && (
<View style={[styles.chip, { backgroundColor: theme.colors.success }]}>
<Text style={[styles.chipText, { color: theme.colors.white }]}>DEBRID</Text>
</View>
)}
{/* Special badge for HDRezka streams */}
{isHDRezka && (
<View style={[styles.chip, { backgroundColor: theme.colors.accent }]}>
<Text style={[styles.chipText, { color: theme.colors.white }]}>HDREZKA</Text>
</View>
)}
</View> </View>
</View> </TouchableOpacity>
</Animated.View>
<View style={styles.streamAction}>
<MaterialIcons
name="play-arrow"
size={24}
color={theme.colors.primary}
/>
</View>
</TouchableOpacity>
); );
}; };
@ -174,44 +183,53 @@ const ProviderFilter = memo(({
}) => { }) => {
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]); const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
const renderItem = useCallback(({ item }: { item: { id: string; name: string } }) => ( const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => (
<TouchableOpacity <Animated.View
key={item.id} entering={FadeIn.duration(300).delay(100 + index * 40)}
style={[ layout={Layout.springify()}
styles.filterChip,
selectedProvider === item.id && styles.filterChipSelected
]}
onPress={() => onSelect(item.id)}
> >
<Text style={[ <TouchableOpacity
styles.filterChipText, key={item.id}
selectedProvider === item.id && styles.filterChipTextSelected style={[
]}> styles.filterChip,
{item.name} selectedProvider === item.id && styles.filterChipSelected
</Text> ]}
</TouchableOpacity> onPress={() => onSelect(item.id)}
>
<Text style={[
styles.filterChipText,
selectedProvider === item.id && styles.filterChipTextSelected
]}>
{item.name}
</Text>
</TouchableOpacity>
</Animated.View>
), [selectedProvider, onSelect, styles]); ), [selectedProvider, onSelect, styles]);
return ( return (
<FlatList <Animated.View
data={providers} entering={FadeIn.duration(300)}
renderItem={renderItem} >
keyExtractor={item => item.id} <FlatList
horizontal data={providers}
showsHorizontalScrollIndicator={false} renderItem={renderItem}
style={styles.filterScroll} keyExtractor={item => item.id}
bounces={true} horizontal
overScrollMode="never" showsHorizontalScrollIndicator={false}
decelerationRate="fast" style={styles.filterScroll}
initialNumToRender={5} bounces={true}
maxToRenderPerBatch={3} overScrollMode="never"
windowSize={3} decelerationRate="fast"
getItemLayout={(data, index) => ({ initialNumToRender={5}
length: 100, // Approximate width of each item maxToRenderPerBatch={3}
offset: 100 * index, windowSize={3}
index, getItemLayout={(data, index) => ({
})} length: 100, // Approximate width of each item
/> offset: 100 * index,
index,
})}
/>
</Animated.View>
); );
}); });
@ -807,13 +825,14 @@ export const StreamsScreen = () => {
); );
}, [handleStreamPress, loadingProviders, providerStatus, currentTheme]); }, [handleStreamPress, loadingProviders, providerStatus, currentTheme]);
const renderSectionHeader = useCallback(({ section }: { section: { title: string } }) => ( const renderSectionHeader = useCallback(({ section }: { section: { title: string; addonId: string } }) => (
<Animated.View <Animated.View
entering={FadeIn.duration(300)} entering={FadeIn.duration(400)}
layout={Layout.springify()}
> >
<Text style={styles.streamGroupTitle}>{section.title}</Text> <Text style={styles.streamGroupTitle}>{section.title}</Text>
</Animated.View> </Animated.View>
), []); ), [styles.streamGroupTitle]);
return ( return (
<View style={styles.container}> <View style={styles.container}>