mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-19 15:52:03 +00:00
ui fix
This commit is contained in:
parent
0e14d257ad
commit
91a42e6e29
2 changed files with 45 additions and 25 deletions
|
|
@ -68,6 +68,9 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
|
|
||||||
const interval = useMemo(() => cardWidth + 16, [cardWidth]);
|
const interval = useMemo(() => cardWidth + 16, [cardWidth]);
|
||||||
|
|
||||||
|
// Reduce top padding on phones while keeping tablets unchanged
|
||||||
|
const effectiveTopOffset = useMemo(() => (isTablet ? TOP_TABS_OFFSET : 8), [isTablet]);
|
||||||
|
|
||||||
const data = useMemo(() => (items && items.length ? items.slice(0, 10) : []), [items]);
|
const data = useMemo(() => (items && items.length ? items.slice(0, 10) : []), [items]);
|
||||||
const loopingEnabled = data.length > 1;
|
const loopingEnabled = data.length > 1;
|
||||||
// Duplicate head/tail for seamless looping
|
// Duplicate head/tail for seamless looping
|
||||||
|
|
@ -241,7 +244,7 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { paddingVertical: 12 }] as StyleProp<ViewStyle>}>
|
<View style={[styles.container, { paddingTop: 12 + effectiveTopOffset }] as StyleProp<ViewStyle>}>
|
||||||
<View style={{ height: cardHeight }}>
|
<View style={{ height: cardHeight }}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
horizontal
|
||||||
|
|
@ -302,7 +305,6 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
key={item.id}
|
|
||||||
style={{ flex: 1 } as any}
|
style={{ flex: 1 } as any}
|
||||||
>
|
>
|
||||||
{Platform.OS === 'android' ? (
|
{Platform.OS === 'android' ? (
|
||||||
|
|
@ -351,7 +353,7 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View entering={FadeIn.duration(150).easing(Easing.out(Easing.cubic))}>
|
<Animated.View entering={FadeIn.duration(150).easing(Easing.out(Easing.cubic))}>
|
||||||
<Animated.View style={[styles.container as ViewStyle, { paddingTop: 12 + TOP_TABS_OFFSET }]}>
|
<Animated.View style={[styles.container as ViewStyle, { paddingTop: 12 + effectiveTopOffset }]}>
|
||||||
{/* Removed preload images for performance - let FastImage cache handle it naturally */}
|
{/* Removed preload images for performance - let FastImage cache handle it naturally */}
|
||||||
{settings.enableHomeHeroBackground && data[activeIndex] && (
|
{settings.enableHomeHeroBackground && data[activeIndex] && (
|
||||||
<BackgroundImage
|
<BackgroundImage
|
||||||
|
|
@ -381,10 +383,11 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
pagingEnabled={false}
|
pagingEnabled={false}
|
||||||
bounces={false}
|
bounces={false}
|
||||||
overScrollMode="never"
|
overScrollMode="never"
|
||||||
onMomentumScrollEnd={() => {
|
onMomentumScrollEnd={(e) => {
|
||||||
if (!loopingEnabled) return;
|
if (!loopingEnabled) return;
|
||||||
// Determine current page index in cloned space
|
// Determine current page index in cloned space
|
||||||
const page = Math.round(scrollX.value / interval);
|
const x = e?.nativeEvent?.contentOffset?.x ?? 0;
|
||||||
|
const page = Math.round(x / interval);
|
||||||
// If at leading clone (0), jump to last real item
|
// If at leading clone (0), jump to last real item
|
||||||
if (page === 0) {
|
if (page === 0) {
|
||||||
scrollToLogicalIndex(data.length - 1, false);
|
scrollToLogicalIndex(data.length - 1, false);
|
||||||
|
|
@ -398,7 +401,7 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
>
|
>
|
||||||
{(loopingEnabled ? loopData : data).map((item, index) => (
|
{(loopingEnabled ? loopData : data).map((item, index) => (
|
||||||
<CarouselCard
|
<CarouselCard
|
||||||
key={item.id}
|
key={`${item.id}-${index}-${loopingEnabled ? 'loop' : 'base'}`}
|
||||||
item={item}
|
item={item}
|
||||||
colors={currentTheme.colors}
|
colors={currentTheme.colors}
|
||||||
logoFailed={failedLogoIds.has(item.id)}
|
logoFailed={failedLogoIds.has(item.id)}
|
||||||
|
|
@ -416,9 +419,9 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
))}
|
))}
|
||||||
</Animated.ScrollView>
|
</Animated.ScrollView>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
{/* Pagination below the card row (animated like FeaturedContent) */}
|
{/* Pagination below the card row (library-based, worklet-driven) */}
|
||||||
<View style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 6, position: 'relative', zIndex: 1 }} pointerEvents="auto">
|
<View style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 6, position: 'relative', zIndex: 1 }} pointerEvents="auto">
|
||||||
<Pagination.Custom
|
<Pagination.Basic
|
||||||
progress={paginationProgress}
|
progress={paginationProgress}
|
||||||
data={data}
|
data={data}
|
||||||
size={10}
|
size={10}
|
||||||
|
|
@ -427,7 +430,6 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
height: 8,
|
height: 8,
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
backgroundColor: currentTheme.colors.elevation3,
|
backgroundColor: currentTheme.colors.elevation3,
|
||||||
opacity: 0.9,
|
|
||||||
}}
|
}}
|
||||||
activeDotStyle={{
|
activeDotStyle={{
|
||||||
width: 10,
|
width: 10,
|
||||||
|
|
@ -440,15 +442,6 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
||||||
onPress={(index: number) => {
|
onPress={(index: number) => {
|
||||||
scrollToLogicalIndex(index, true);
|
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>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
@ -570,8 +563,8 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
|
||||||
// AGGRESSIVE early exit for cards far from center
|
// AGGRESSIVE early exit for cards far from center
|
||||||
if (distance > interval * 1.5) {
|
if (distance > interval * 1.5) {
|
||||||
return {
|
return {
|
||||||
transform: [{ scale: 0.9 }],
|
transform: [{ scale: isTablet ? 0.95 : 0.9 }],
|
||||||
opacity: 0.7
|
opacity: isTablet ? 0.85 : 0.7
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -579,11 +572,11 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
|
||||||
|
|
||||||
// Scale animation based on distance from center
|
// Scale animation based on distance from center
|
||||||
const scale = 1 - (distance / maxDistance) * 0.1;
|
const scale = 1 - (distance / maxDistance) * 0.1;
|
||||||
const clampedScale = Math.max(0.9, Math.min(1, scale));
|
const clampedScale = Math.max(isTablet ? 0.95 : 0.9, Math.min(1, scale));
|
||||||
|
|
||||||
// Opacity animation for cards that are far from center
|
// Opacity animation for cards that are far from center
|
||||||
const opacity = 1 - (distance / maxDistance) * 0.3;
|
const opacity = 1 - (distance / maxDistance) * 0.3;
|
||||||
const clampedOpacity = Math.max(0.7, Math.min(1, opacity));
|
const clampedOpacity = Math.max(isTablet ? 0.85 : 0.7, Math.min(1, opacity));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transform: [{ scale: clampedScale }],
|
transform: [{ scale: clampedScale }],
|
||||||
|
|
@ -677,7 +670,7 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={["rgba(0,0,0,0.25)", "rgba(0,0,0,0.85)"]}
|
colors={["rgba(0,0,0,0.18)", "rgba(0,0,0,0.72)"]}
|
||||||
locations={[0.3, 1]}
|
locations={[0.3, 1]}
|
||||||
style={styles.bannerGradient as ViewStyle}
|
style={styles.bannerGradient as ViewStyle}
|
||||||
/>
|
/>
|
||||||
|
|
@ -703,7 +696,16 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<ScrollView style={{ maxHeight: 120, width: Math.round(cardWidth * 0.85), alignSelf: 'center' }} showsVerticalScrollIndicator={false}>
|
<ScrollView style={{ maxHeight: 120, width: Math.round(cardWidth * 0.85), alignSelf: 'center' }} showsVerticalScrollIndicator={false}>
|
||||||
<Text style={[styles.backDescription as TextStyle, { color: colors.mediumEmphasis, textAlign: 'center' }]}>
|
<Text style={[
|
||||||
|
styles.backDescription as TextStyle,
|
||||||
|
{
|
||||||
|
color: colors.highEmphasis,
|
||||||
|
textAlign: 'center',
|
||||||
|
textShadowColor: 'rgba(0,0,0,0.6)',
|
||||||
|
textShadowOffset: { width: 0, height: 1 },
|
||||||
|
textShadowRadius: 2,
|
||||||
|
}
|
||||||
|
]}>
|
||||||
{item.description || 'No description available'}
|
{item.description || 'No description available'}
|
||||||
</Text>
|
</Text>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
@ -811,7 +813,15 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<ScrollView style={{ maxHeight: 120 }} showsVerticalScrollIndicator={false}>
|
<ScrollView style={{ maxHeight: 120 }} showsVerticalScrollIndicator={false}>
|
||||||
<Text style={[styles.backDescription as TextStyle, { color: colors.mediumEmphasis }]}>
|
<Text style={[
|
||||||
|
styles.backDescription as TextStyle,
|
||||||
|
{
|
||||||
|
color: colors.highEmphasis,
|
||||||
|
textShadowColor: 'rgba(0,0,0,0.6)',
|
||||||
|
textShadowOffset: { width: 0, height: 1 },
|
||||||
|
textShadowRadius: 2,
|
||||||
|
}
|
||||||
|
]}>
|
||||||
{item.description || 'No description available'}
|
{item.description || 'No description available'}
|
||||||
</Text>
|
</Text>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@ const HomeScreenSettings: React.FC = () => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const [showSavedIndicator, setShowSavedIndicator] = useState(false);
|
const [showSavedIndicator, setShowSavedIndicator] = useState(false);
|
||||||
const fadeAnim = React.useRef(new Animated.Value(0)).current;
|
const fadeAnim = React.useRef(new Animated.Value(0)).current;
|
||||||
|
const isTabletDevice = Platform.OS !== 'web' && (Dimensions.get('window').width >= 768);
|
||||||
|
|
||||||
// Prevent iOS entrance flicker by restoring a non-translucent StatusBar
|
// Prevent iOS entrance flicker by restoring a non-translucent StatusBar
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
|
|
@ -162,6 +163,15 @@ const HomeScreenSettings: React.FC = () => {
|
||||||
setShowSavedIndicator(true);
|
setShowSavedIndicator(true);
|
||||||
}, [updateSetting]);
|
}, [updateSetting]);
|
||||||
|
|
||||||
|
// Ensure carousel is the default hero layout on tablets for all users
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
if (isTabletDevice && settings.heroStyle !== 'carousel') {
|
||||||
|
updateSetting('heroStyle', 'carousel' as any);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, [isTabletDevice, settings.heroStyle, updateSetting]);
|
||||||
|
|
||||||
const CustomSwitch = ({ value, onValueChange }: { value: boolean, onValueChange: (value: boolean) => void }) => (
|
const CustomSwitch = ({ value, onValueChange }: { value: boolean, onValueChange: (value: boolean) => void }) => (
|
||||||
<Switch
|
<Switch
|
||||||
value={value}
|
value={value}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue