diff --git a/src/components/home/ContentItem.tsx b/src/components/home/ContentItem.tsx
index 3738c446..f4ff1bce 100644
--- a/src/components/home/ContentItem.tsx
+++ b/src/components/home/ContentItem.tsx
@@ -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 (
+
+
+ {/* Reserve space for title to keep section spacing stable */}
+
+
+ );
+ }
+
return (
<>
-
+
{
@@ -227,7 +259,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
{item.name}
)}
-
+
{
const [settings, setSettings] = useState(DEFAULT_SETTINGS);
+ const [isLoaded, setIsLoaded] = useState(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 (
@@ -217,6 +222,7 @@ export const useSettings = () => {
return {
settings,
updateSetting,
+ isLoaded,
};
};
diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx
index e4cea5f2..a40fbc80 100644
--- a/src/screens/LibraryScreen.tsx
+++ b/src/screens/LibraryScreen.tsx
@@ -209,7 +209,7 @@ const LibraryScreen = () => {
const { numColumns, itemWidth } = useMemo(() => getGridLayout(width), [width]);
const [loading, setLoading] = useState(true);
const [libraryItems, setLibraryItems] = useState([]);
- 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(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}
>
-
+ {filterType === 'trakt' ? (
+
+
+
+ ) : (
+
+ )}
{
return ;
}
- // 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 (
-
-
- Your library is empty
-
- Add content to your library to keep track of what you're watching
-
- navigation.navigate('MainTabs')}
- activeOpacity={0.7}
- >
- Explore Content
-
-
- );
+ if (filteredItems.length === 0) {
+ // Intentionally render nothing to match the minimal empty state in the design
+ return ;
}
return (
- {
- 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={() => {}}
- />
+ 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')}