mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
Homescreen layout and visual glithches optimization.
This commit is contained in:
parent
a0626e8f8a
commit
59c0b6ba1b
3 changed files with 86 additions and 69 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue