Homescreen layout and visual glithches optimization.

This commit is contained in:
tapframe 2025-09-17 13:36:05 +05:30
parent a0626e8f8a
commit 59c0b6ba1b
3 changed files with 86 additions and 69 deletions

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, Platform, Text } from 'react-native';
import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, Platform, Text, Animated } from 'react-native';
import { Image as ExpoImage } from 'expo-image';
import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../../contexts/ThemeContext';
@ -66,8 +66,9 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
const [shouldLoadImageState, setShouldLoadImageState] = useState(false);
const [retryCount, setRetryCount] = useState(0);
const { currentTheme } = useTheme();
const { settings } = useSettings();
const { settings, isLoaded } = useSettings();
const posterRadius = typeof settings.posterBorderRadius === 'number' ? settings.posterBorderRadius : 12;
const fadeInOpacity = React.useRef(new Animated.Value(0)).current;
// Memoize poster width calculation to avoid recalculating on every render
const posterWidth = React.useMemo(() => {
switch (settings.posterSize) {
@ -157,9 +158,42 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
return item.poster;
}, [item.poster, retryCount, item.id]);
// Smoothly fade in content when settings are ready
useEffect(() => {
if (isLoaded) {
fadeInOpacity.setValue(0);
Animated.timing(fadeInOpacity, {
toValue: 1,
duration: 180,
useNativeDriver: true,
}).start();
}
}, [isLoaded, fadeInOpacity]);
// While settings load, render a placeholder with reserved space (poster aspect + title)
if (!isLoaded) {
const placeholderRadius = 12;
return (
<View style={[styles.itemContainer, { width: posterWidth }]}>
<View
style={[
styles.contentItem,
{
width: posterWidth,
borderRadius: placeholderRadius,
backgroundColor: currentTheme.colors.elevation1,
},
]}
/>
{/* Reserve space for title to keep section spacing stable */}
<View style={{ height: 18, marginTop: 4 }} />
</View>
);
}
return (
<>
<View style={[styles.itemContainer, { width: posterWidth }]}>
<Animated.View style={[styles.itemContainer, { width: posterWidth, opacity: fadeInOpacity }]}>
<TouchableOpacity
style={[styles.contentItem, { width: posterWidth, borderRadius: posterRadius }]}
activeOpacity={0.7}
@ -175,9 +209,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, borderRadius: posterRadius }]}
contentFit="cover"
cachePolicy={Platform.OS === 'android' ? 'disk' : 'memory-disk'}
transition={0} // Disable transition to reduce GPU work
placeholder={{ blurhash: PLACEHOLDER_BLURHASH } as any}
placeholderContentFit="cover"
transition={140}
allowDownscaling
priority="low" // Deprioritize decode for long lists
onLoad={() => {
@ -227,7 +259,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
{item.name}
</Text>
)}
</View>
</Animated.View>
<DropUpMenu
visible={menuVisible}

View file

@ -126,6 +126,7 @@ const SETTINGS_STORAGE_KEY = 'app_settings';
export const useSettings = () => {
const [settings, setSettings] = useState<AppSettings>(DEFAULT_SETTINGS);
const [isLoaded, setIsLoaded] = useState<boolean>(false);
useEffect(() => {
loadSettings();
@ -180,6 +181,10 @@ export const useSettings = () => {
// Fallback to default settings on error
setSettings(DEFAULT_SETTINGS);
}
finally {
// Mark settings as loaded so UI can render with correct values without flicker
setIsLoaded(true);
}
};
const updateSetting = useCallback(async <K extends keyof AppSettings>(
@ -217,6 +222,7 @@ export const useSettings = () => {
return {
settings,
updateSetting,
isLoaded,
};
};

View file

@ -209,7 +209,7 @@ const LibraryScreen = () => {
const { numColumns, itemWidth } = useMemo(() => getGridLayout(width), [width]);
const [loading, setLoading] = useState(true);
const [libraryItems, setLibraryItems] = useState<LibraryItem[]>([]);
const [filter, setFilter] = useState<'all' | 'movies' | 'series'>('all');
const [filter, setFilter] = useState<'trakt' | 'movies' | 'series'>('movies');
const [showTraktContent, setShowTraktContent] = useState(false);
const [selectedTraktFolder, setSelectedTraktFolder] = useState<string | null>(null);
const insets = useSafeAreaInsets();
@ -295,7 +295,6 @@ const LibraryScreen = () => {
}, []);
const filteredItems = libraryItems.filter(item => {
if (filter === 'all') return true;
if (filter === 'movies') return item.type === 'movie';
if (filter === 'series') return item.type === 'series';
return true;
@ -776,7 +775,7 @@ const LibraryScreen = () => {
);
};
const renderFilter = (filterType: 'all' | 'movies' | 'series', label: string, iconName: keyof typeof MaterialIcons.glyphMap) => {
const renderFilter = (filterType: 'trakt' | 'movies' | 'series', label: string, iconName: keyof typeof MaterialIcons.glyphMap) => {
const isActive = filter === filterType;
return (
@ -786,15 +785,33 @@ const LibraryScreen = () => {
isActive && { backgroundColor: currentTheme.colors.primary },
{ shadowColor: currentTheme.colors.black }
]}
onPress={() => setFilter(filterType)}
onPress={() => {
if (filterType === 'trakt') {
if (!traktAuthenticated) {
navigation.navigate('TraktSettings');
} else {
setShowTraktContent(true);
setSelectedTraktFolder(null);
loadAllCollections();
}
return;
}
setFilter(filterType);
}}
activeOpacity={0.7}
>
<MaterialIcons
name={iconName}
size={22}
color={isActive ? currentTheme.colors.white : currentTheme.colors.mediumGray}
style={styles.filterIcon}
/>
{filterType === 'trakt' ? (
<View style={[styles.filterIcon, { justifyContent: 'center', alignItems: 'center' }]}>
<TraktIcon width={18} height={18} style={{ opacity: isActive ? 1 : 0.6 }} />
</View>
) : (
<MaterialIcons
name={iconName}
size={22}
color={isActive ? currentTheme.colors.white : currentTheme.colors.mediumGray}
style={styles.filterIcon}
/>
)}
<Text
style={[
styles.filterText,
@ -813,60 +830,22 @@ const LibraryScreen = () => {
return <SkeletonLoader />;
}
// Combine regular library items with Trakt folder
const allItems = [];
// Add Trakt folder if authenticated or as connection prompt
if (traktAuthenticated || !traktAuthenticated) {
allItems.push({ type: 'trakt-folder', id: 'trakt-folder' });
}
// Add filtered library items
allItems.push(...filteredItems);
if (allItems.length === 0) {
return (
<View style={styles.emptyContainer}>
<MaterialIcons
name="video-library"
size={80}
color={currentTheme.colors.mediumGray}
style={{ opacity: 0.7 }}
/>
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>Your library is empty</Text>
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
Add content to your library to keep track of what you're watching
</Text>
<TouchableOpacity
style={[styles.exploreButton, {
backgroundColor: currentTheme.colors.primary,
shadowColor: currentTheme.colors.black
}]}
onPress={() => navigation.navigate('MainTabs')}
activeOpacity={0.7}
>
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Explore Content</Text>
</TouchableOpacity>
</View>
);
if (filteredItems.length === 0) {
// Intentionally render nothing to match the minimal empty state in the design
return <View style={styles.listContainer} />;
}
return (
<FlashList
data={allItems}
renderItem={({ item }) => {
if (item.type === 'trakt-folder') {
return renderTraktFolder();
}
return renderItem({ item: item as LibraryItem });
}}
keyExtractor={item => item.id}
numColumns={numColumns}
contentContainerStyle={styles.listContainer}
showsVerticalScrollIndicator={false}
onEndReachedThreshold={0.7}
onEndReached={() => {}}
/>
<FlashList
data={filteredItems}
renderItem={({ item }) => renderItem({ item: item as LibraryItem })}
keyExtractor={item => item.id}
numColumns={numColumns}
contentContainerStyle={styles.listContainer}
showsVerticalScrollIndicator={false}
onEndReachedThreshold={0.7}
onEndReached={() => {}}
/>
);
};
@ -937,7 +916,7 @@ const LibraryScreen = () => {
contentContainerStyle={styles.filtersContainer}
style={styles.filtersScrollView}
>
{renderFilter('all', 'All', 'apps')}
{renderFilter('trakt', 'Trakt', 'pan-tool')}
{renderFilter('movies', 'Movies', 'movie')}
{renderFilter('series', 'TV Shows', 'live-tv')}
</ScrollView>