minor ui changes

This commit is contained in:
tapframe 2025-08-26 14:49:23 +05:30
parent cabfedad23
commit 8c7563590b
6 changed files with 211 additions and 17 deletions

@ -1 +1 @@
Subproject commit 6591e3bc12a64a191367829d3fa5eb3783085c95
Subproject commit f56983d17df532f0342cf9bb9d11ae74f64637ff

View file

@ -38,6 +38,7 @@ interface FeaturedContentProps {
featuredContent: StreamingContent | null;
isSaved: boolean;
handleSaveToLibrary: () => void;
loading?: boolean;
}
// Cache to store preloaded images
@ -122,7 +123,7 @@ const NoFeaturedContent = () => {
);
};
const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: FeaturedContentProps) => {
const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loading }: FeaturedContentProps) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const [bannerUrl, setBannerUrl] = useState<string | null>(null);
@ -184,8 +185,18 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
// Simplified validation to reduce CPU overhead
if (!url || typeof url !== 'string') return false;
// Use our optimized cache service instead of direct prefetch
await imageCacheService.getCachedImageUrl(url);
// Add timeout guard to prevent hanging preloads
const timeout = new Promise<never>((_, reject) => {
const t = setTimeout(() => {
clearTimeout(t as any);
reject(new Error('preload-timeout'));
}, 1500);
});
await Promise.race([
imageCacheService.getCachedImageUrl(url),
timeout,
]);
imageCache[url] = true;
return true;
} catch (error) {
@ -446,7 +457,13 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
}
};
// Show skeleton while loading to avoid empty state flash and sluggish feel
if (loading) {
return <SkeletonFeatured />;
}
if (!featuredContent) {
// Suppress empty state while loading to avoid flash on startup/hydration
return <NoFeaturedContent />;
}

View file

@ -1,9 +1,11 @@
import React from 'react';
import { View, Text, ActivityIndicator, StyleSheet, Dimensions } from 'react-native';
import React, { useEffect, useRef } from 'react';
import { View, Text, ActivityIndicator, StyleSheet, Dimensions, Animated, Easing } from 'react-native';
import { useTheme } from '../../contexts/ThemeContext';
import type { Theme } from '../../contexts/ThemeContext';
import { LinearGradient } from 'expo-linear-gradient';
const { height } = Dimensions.get('window');
const { width, height } = Dimensions.get('window');
const isTablet = width >= 768;
export const SkeletonCatalog = () => {
const { currentTheme } = useTheme();
@ -18,10 +20,74 @@ export const SkeletonCatalog = () => {
export const SkeletonFeatured = () => {
const { currentTheme } = useTheme();
const shimmerAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
const loop = Animated.loop(
Animated.timing(shimmerAnim, {
toValue: 1,
duration: 1400,
easing: Easing.linear,
useNativeDriver: true,
})
);
loop.start();
return () => loop.stop();
}, [shimmerAnim]);
const translateX = shimmerAnim.interpolate({
inputRange: [0, 1],
outputRange: [-width, width],
});
return (
<View style={[styles.featuredLoadingContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
<Text style={[styles.loadingText, { color: currentTheme.colors.textMuted }]}>Loading featured content...</Text>
<View style={[styles.featuredSkeletonContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
{/* Shimmer overlay */}
<Animated.View style={[styles.shimmerOverlay, { transform: [{ translateX }] }]}>
<LinearGradient
colors={[
'rgba(255,255,255,0)',
'rgba(255,255,255,0.08)',
'rgba(255,255,255,0)'
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={StyleSheet.absoluteFillObject}
/>
</Animated.View>
{/* Bottom gradient to mimic hero */}
<LinearGradient
colors={[
'rgba(0,0,0,0.10)',
'rgba(0,0,0,0.20)',
'rgba(0,0,0,0.40)',
'rgba(0,0,0,0.80)',
currentTheme.colors.darkBackground,
]}
locations={[0, 0.2, 0.5, 0.8, 1]}
style={StyleSheet.absoluteFillObject}
/>
{/* Placeholder content near bottom like hero */}
<View style={styles.featuredSkeletonContent}>
{/* Logo/title bar */}
<View style={[styles.logoBar, { backgroundColor: currentTheme.colors.elevation2 }]} />
{/* Genre dots */}
<View style={styles.genreRow}>
<View style={[styles.genreDot, { backgroundColor: currentTheme.colors.elevation2 }]} />
<View style={[styles.genreDot, { backgroundColor: currentTheme.colors.elevation2 }]} />
<View style={[styles.genreDot, { backgroundColor: currentTheme.colors.elevation2 }]} />
</View>
{/* Buttons row */}
<View style={styles.buttonsRow}>
<View style={[styles.circleBtn, { backgroundColor: currentTheme.colors.elevation2 }]} />
<View style={[styles.primaryBtn, { backgroundColor: currentTheme.colors.white }]} />
<View style={[styles.circleBtn, { backgroundColor: currentTheme.colors.elevation2 }]} />
</View>
</View>
</View>
);
};
@ -39,11 +105,69 @@ const styles = StyleSheet.create({
borderRadius: 12,
marginHorizontal: 16,
},
featuredLoadingContainer: {
height: height * 0.4,
justifyContent: 'center',
featuredSkeletonContainer: {
width: '100%',
height: isTablet ? height * 0.7 : height * 0.55,
marginTop: 0,
marginBottom: 12,
position: 'relative',
borderRadius: isTablet ? 16 : 12,
overflow: 'hidden',
},
shimmerOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
},
featuredSkeletonContent: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
paddingHorizontal: isTablet ? 40 : 20,
paddingBottom: isTablet ? 50 : 16,
alignItems: 'center',
},
logoBar: {
width: isTablet ? width * 0.32 : width * 0.8,
height: isTablet ? 28 : 22,
borderRadius: 6,
marginBottom: isTablet ? 20 : 12,
},
genreRow: {
flexDirection: 'row',
gap: 8,
marginBottom: isTablet ? 28 : 12,
},
genreDot: {
width: 50,
height: 12,
borderRadius: 6,
opacity: 0.8,
},
buttonsRow: {
width: '100%',
flexDirection: 'row',
justifyContent: 'space-evenly',
alignItems: 'center',
minHeight: isTablet ? 70 : 60,
paddingTop: 8,
paddingBottom: 8,
},
circleBtn: {
width: isTablet ? 48 : 44,
height: isTablet ? 48 : 44,
borderRadius: 100,
opacity: 0.9,
},
primaryBtn: {
width: isTablet ? 180 : 140,
height: isTablet ? 48 : 44,
borderRadius: 30,
opacity: 0.95,
},
loadingText: {
marginTop: 12,
fontSize: 14,

View file

@ -1,4 +1,5 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { StreamingContent, catalogService } from '../services/catalogService';
import { tmdbService } from '../services/tmdbService';
import { logger } from '../utils/logger';
@ -22,6 +23,7 @@ const persistentStore = {
// Cache timeout in milliseconds (e.g., 5 minutes)
const CACHE_TIMEOUT = 5 * 60 * 1000;
const STORAGE_KEY = 'featured_content_cache_v1';
export function useFeaturedContent() {
const [featuredContent, setFeaturedContent] = useState<StreamingContent | null>(persistentStore.featuredContent);
@ -167,6 +169,22 @@ export function useFeaturedContent() {
if (signal.aborted) return;
// Safety guard: if nothing came back within a reasonable time, stop loading
if (!formattedContent || formattedContent.length === 0) {
// Fall back to any cached featured item so UI can render something
const cachedJson = await AsyncStorage.getItem(STORAGE_KEY).catch(() => null);
if (cachedJson) {
try {
const parsed = JSON.parse(cachedJson);
if (parsed?.featuredContent) {
formattedContent = Array.isArray(parsed.allFeaturedContent) && parsed.allFeaturedContent.length > 0
? parsed.allFeaturedContent
: [parsed.featuredContent];
}
} catch {}
}
}
// Update persistent store with the new data
persistentStore.allFeaturedContent = formattedContent;
persistentStore.lastFetchTime = now;
@ -178,9 +196,22 @@ export function useFeaturedContent() {
persistentStore.featuredContent = formattedContent[0];
setFeaturedContent(formattedContent[0]);
currentIndexRef.current = 0;
// Persist cache for fast startup
try {
await AsyncStorage.setItem(
STORAGE_KEY,
JSON.stringify({
ts: now,
featuredContent: formattedContent[0],
allFeaturedContent: formattedContent,
})
);
} catch {}
} else {
persistentStore.featuredContent = null;
setFeaturedContent(null);
// Clear persisted cache on empty
try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {}
}
} catch (error) {
if (signal.aborted) {
@ -197,6 +228,29 @@ export function useFeaturedContent() {
}
}, [cleanup, genreMap, loadingGenres, contentSource, selectedCatalogs]);
// Hydrate from persisted cache immediately for instant render
useEffect(() => {
let cancelled = false;
(async () => {
try {
const json = await AsyncStorage.getItem(STORAGE_KEY);
if (!json) return;
const parsed = JSON.parse(json);
if (cancelled) return;
if (parsed?.featuredContent) {
persistentStore.featuredContent = parsed.featuredContent;
persistentStore.allFeaturedContent = Array.isArray(parsed.allFeaturedContent) ? parsed.allFeaturedContent : [];
persistentStore.lastFetchTime = typeof parsed.ts === 'number' ? parsed.ts : Date.now();
persistentStore.isFirstLoad = false;
setFeaturedContent(parsed.featuredContent);
setAllFeaturedContent(persistentStore.allFeaturedContent);
setLoading(false);
}
} catch {}
})();
return () => { cancelled = true; };
}, []);
// Check for settings changes, including during app restart
useEffect(() => {
// Check if settings changed while app was closed

View file

@ -10,7 +10,6 @@ import { MaterialCommunityIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { BlurView } from 'expo-blur';
import { colors } from '../styles/colors';
import { NuvioHeader } from '../components/NuvioHeader';
import { Stream } from '../types/streams';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { useTheme } from '../contexts/ThemeContext';
@ -587,8 +586,7 @@ const MainTabs = () => {
],
},
}),
header: () => (route.name === 'Home' ? <NuvioHeader /> : null),
headerShown: route.name === 'Home',
headerShown: false,
tabBarShowLabel: false,
tabBarStyle: {
position: 'absolute',

View file

@ -603,6 +603,7 @@ const HomeScreen = () => {
featuredContent={featuredContent}
isSaved={isSaved}
handleSaveToLibrary={handleSaveToLibrary}
loading={featuredLoading}
/>
), [showHeroSection, featuredContentSource, featuredContent, isSaved, handleSaveToLibrary]);
@ -714,7 +715,7 @@ const HomeScreen = () => {
keyExtractor={item => item.key}
contentContainerStyle={[
styles.scrollContent,
{ paddingTop: Platform.OS === 'ios' ? 100 : 90 }
{ paddingTop: insets.top }
]}
showsVerticalScrollIndicator={false}
ListHeaderComponent={showHeroSection ? memoizedFeaturedContent : null}