mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 08:41:57 +00:00
changes
This commit is contained in:
parent
a5a66a5e8c
commit
5c3c5717ab
1 changed files with 91 additions and 9 deletions
|
|
@ -1,9 +1,10 @@
|
||||||
import React, { useMemo, useState, useEffect, useCallback, memo, useRef } from 'react';
|
import React, { useMemo, useState, useEffect, useCallback, memo, useRef } from 'react';
|
||||||
import { View, Text, StyleSheet, Dimensions, TouchableOpacity, ViewStyle, TextStyle, ImageStyle, ScrollView, StyleProp, Platform, Image } from 'react-native';
|
import { View, Text, StyleSheet, Dimensions, TouchableOpacity, ViewStyle, TextStyle, ImageStyle, ScrollView, StyleProp, Platform, Image } from 'react-native';
|
||||||
import Animated, { FadeIn, FadeOut, Easing, useSharedValue, withTiming, useAnimatedStyle, useAnimatedScrollHandler, useAnimatedReaction, runOnJS, SharedValue } from 'react-native-reanimated';
|
import Animated, { FadeIn, FadeOut, Easing, useSharedValue, withTiming, useAnimatedStyle, useAnimatedScrollHandler, useAnimatedReaction, runOnJS, SharedValue, interpolate, Extrapolation } from 'react-native-reanimated';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { BlurView } from 'expo-blur';
|
import { BlurView } from 'expo-blur';
|
||||||
import FastImage from '@d11/react-native-fast-image';
|
import FastImage from '@d11/react-native-fast-image';
|
||||||
|
import { Pagination } from 'react-native-reanimated-carousel';
|
||||||
|
|
||||||
// Optional iOS Glass effect (expo-glass-effect) with safe fallback for HeroCarousel
|
// Optional iOS Glass effect (expo-glass-effect) with safe fallback for HeroCarousel
|
||||||
let GlassViewComp: any = null;
|
let GlassViewComp: any = null;
|
||||||
|
|
@ -43,7 +44,15 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
|
|
||||||
const data = useMemo(() => (items && items.length ? items.slice(0, 5) : []), [items]);
|
const data = useMemo(() => (items && items.length ? items.slice(0, 10) : []), [items]);
|
||||||
|
const loopingEnabled = data.length > 1;
|
||||||
|
// Duplicate head/tail for seamless looping
|
||||||
|
const loopData = useMemo(() => {
|
||||||
|
if (!loopingEnabled) return data;
|
||||||
|
const head = data[0];
|
||||||
|
const tail = data[data.length - 1];
|
||||||
|
return [tail, ...data, head];
|
||||||
|
}, [data, loopingEnabled]);
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
const [failedLogoIds, setFailedLogoIds] = useState<Set<string>>(new Set());
|
const [failedLogoIds, setFailedLogoIds] = useState<Set<string>>(new Set());
|
||||||
const scrollViewRef = useRef<any>(null);
|
const scrollViewRef = useRef<any>(null);
|
||||||
|
|
@ -55,9 +64,10 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
// Optimized: update background as soon as scroll starts, without waiting for momentum end
|
// Optimized: update background as soon as scroll starts, without waiting for momentum end
|
||||||
const scrollX = useSharedValue(0);
|
const scrollX = useSharedValue(0);
|
||||||
const interval = CARD_WIDTH + 16;
|
const interval = CARD_WIDTH + 16;
|
||||||
|
const paginationProgress = useSharedValue(0);
|
||||||
|
|
||||||
// Parallel image prefetch: start fetching banners and logos as soon as data arrives
|
// Parallel image prefetch: start fetching banners and logos as soon as data arrives
|
||||||
const itemsToPreload = useMemo(() => data.slice(0, 8), [data]);
|
const itemsToPreload = useMemo(() => data.slice(0, 12), [data]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!itemsToPreload.length) return;
|
if (!itemsToPreload.length) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -84,12 +94,13 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
|
|
||||||
// Comprehensive reset when component mounts/remounts to prevent glitching
|
// Comprehensive reset when component mounts/remounts to prevent glitching
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollX.value = 0;
|
// Start at the first real item for looping
|
||||||
|
scrollX.value = loopingEnabled ? interval : 0;
|
||||||
setActiveIndex(0);
|
setActiveIndex(0);
|
||||||
|
|
||||||
// Scroll to position 0 after a brief delay to ensure ScrollView is ready
|
// Scroll to position 0 after a brief delay to ensure ScrollView is ready
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
scrollViewRef.current?.scrollTo({ x: 0, y: 0, animated: false });
|
scrollViewRef.current?.scrollTo({ x: loopingEnabled ? interval : 0, y: 0, animated: false });
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
|
|
@ -98,11 +109,11 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
// Reset scroll when data becomes available
|
// Reset scroll when data becomes available
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
scrollX.value = 0;
|
scrollX.value = loopingEnabled ? interval : 0;
|
||||||
setActiveIndex(0);
|
setActiveIndex(0);
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
scrollViewRef.current?.scrollTo({ x: 0, y: 0, animated: false });
|
scrollViewRef.current?.scrollTo({ x: loopingEnabled ? interval : 0, y: 0, animated: false });
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
|
|
@ -131,7 +142,13 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
const lastIndexUpdateRef = useRef(0);
|
const lastIndexUpdateRef = useRef(0);
|
||||||
useAnimatedReaction(
|
useAnimatedReaction(
|
||||||
() => {
|
() => {
|
||||||
const idx = Math.round(scrollX.value / interval);
|
// Convert scroll position to logical data index (exclude duplicated items)
|
||||||
|
let idx = Math.round(scrollX.value / interval);
|
||||||
|
if (loopingEnabled) {
|
||||||
|
idx -= 1; // account for leading duplicate
|
||||||
|
}
|
||||||
|
if (idx < 0) idx = data.length - 1;
|
||||||
|
if (idx > data.length - 1) idx = 0;
|
||||||
return idx;
|
return idx;
|
||||||
},
|
},
|
||||||
(idx, prevIdx) => {
|
(idx, prevIdx) => {
|
||||||
|
|
@ -149,6 +166,22 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
[data.length]
|
[data.length]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Keep pagination progress in sync with scrollX so we can animate dots like FeaturedContent
|
||||||
|
useAnimatedReaction(
|
||||||
|
() => scrollX.value / interval,
|
||||||
|
(val) => {
|
||||||
|
// Align pagination progress with logical index space
|
||||||
|
paginationProgress.value = loopingEnabled ? val - 1 : val;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// JS helper to jump without flicker when hitting clones
|
||||||
|
const scrollToLogicalIndex = useCallback((logicalIndex: number, animated = true) => {
|
||||||
|
const target = loopingEnabled ? (logicalIndex + 1) * interval : logicalIndex * interval;
|
||||||
|
scrollViewRef.current?.scrollTo({ x: target, y: 0, animated });
|
||||||
|
}, [interval, loopingEnabled]);
|
||||||
|
|
||||||
const contentPadding = useMemo(() => ({ paddingHorizontal: (width - CARD_WIDTH) / 2 }), []);
|
const contentPadding = useMemo(() => ({ paddingHorizontal: (width - CARD_WIDTH) / 2 }), []);
|
||||||
|
|
||||||
const handleNavigateToMetadata = useCallback((id: string, type: any) => {
|
const handleNavigateToMetadata = useCallback((id: string, type: any) => {
|
||||||
|
|
@ -310,8 +343,22 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
pagingEnabled={false}
|
pagingEnabled={false}
|
||||||
bounces={false}
|
bounces={false}
|
||||||
overScrollMode="never"
|
overScrollMode="never"
|
||||||
|
onMomentumScrollEnd={() => {
|
||||||
|
if (!loopingEnabled) return;
|
||||||
|
// Determine current page index in cloned space
|
||||||
|
const page = Math.round(scrollX.value / interval);
|
||||||
|
// If at leading clone (0), jump to last real item
|
||||||
|
if (page === 0) {
|
||||||
|
scrollToLogicalIndex(data.length - 1, false);
|
||||||
|
}
|
||||||
|
// If at trailing clone (last), jump to first real item
|
||||||
|
const lastPage = loopData.length - 1;
|
||||||
|
if (page === lastPage) {
|
||||||
|
scrollToLogicalIndex(0, false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{data.map((item, index) => (
|
{(loopingEnabled ? loopData : data).map((item, index) => (
|
||||||
<CarouselCard
|
<CarouselCard
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
|
|
@ -325,6 +372,41 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
))}
|
))}
|
||||||
</Animated.ScrollView>
|
</Animated.ScrollView>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
{/* Pagination below the card row (animated like FeaturedContent) */}
|
||||||
|
<View style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 6, position: 'relative', zIndex: 1 }} pointerEvents="auto">
|
||||||
|
<Pagination.Custom
|
||||||
|
progress={paginationProgress}
|
||||||
|
data={data}
|
||||||
|
size={10}
|
||||||
|
dotStyle={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 999,
|
||||||
|
backgroundColor: currentTheme.colors.elevation3,
|
||||||
|
opacity: 0.9,
|
||||||
|
}}
|
||||||
|
activeDotStyle={{
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
borderRadius: 999,
|
||||||
|
backgroundColor: currentTheme.colors.white,
|
||||||
|
}}
|
||||||
|
containerStyle={{ gap: 8 }}
|
||||||
|
horizontal
|
||||||
|
onPress={(index: number) => {
|
||||||
|
scrollToLogicalIndex(index, true);
|
||||||
|
}}
|
||||||
|
customReanimatedStyle={(p: number, index: number, length: number) => {
|
||||||
|
'worklet';
|
||||||
|
let v = Math.abs(p - index);
|
||||||
|
if (index === 0 && p > length - 1) {
|
||||||
|
v = Math.abs(p - length);
|
||||||
|
}
|
||||||
|
const scale = interpolate(v, [0, 1], [1.2, 1], Extrapolation.CLAMP);
|
||||||
|
return { transform: [{ scale }] };
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue