Refactor ContentItem and LibraryScreen components for improved state management and performance

This update modifies the ContentItem component to eliminate local state management in favor of direct prop usage, enhancing clarity and reducing unnecessary re-renders. Additionally, the LibraryScreen component is refactored to introduce a new TraktItem component for better separation of concerns and improved poster loading logic. The rendering of Trakt items is optimized with a FlatList for better performance, and placeholder handling is implemented for asynchronous poster loading, ensuring a smoother user experience.
This commit is contained in:
tapframe 2025-06-21 01:43:41 +05:30
parent 152ec88e04
commit 12d625e8d6
2 changed files with 156 additions and 335 deletions

View file

@ -4,7 +4,7 @@ import { Image as ExpoImage } from 'expo-image';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../../contexts/ThemeContext'; import { useTheme } from '../../contexts/ThemeContext';
import { catalogService, StreamingContent } from '../../services/catalogService'; import { catalogService, StreamingContent } from '../../services/catalogService';
import DropUpMenu from './DropUpMenu'; import { DropUpMenu } from './DropUpMenu';
interface ContentItemProps { interface ContentItemProps {
item: StreamingContent; item: StreamingContent;
@ -53,9 +53,8 @@ const calculatePosterLayout = (screenWidth: number) => {
const posterLayout = calculatePosterLayout(width); const posterLayout = calculatePosterLayout(width);
const POSTER_WIDTH = posterLayout.posterWidth; const POSTER_WIDTH = posterLayout.posterWidth;
const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => { const ContentItem = React.memo(({ item, onPress }: ContentItemProps) => {
const [menuVisible, setMenuVisible] = useState(false); const [menuVisible, setMenuVisible] = useState(false);
const [localItem, setLocalItem] = useState(initialItem);
const [isWatched, setIsWatched] = useState(false); const [isWatched, setIsWatched] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false); const [imageLoaded, setImageLoaded] = useState(false);
const [imageError, setImageError] = useState(false); const [imageError, setImageError] = useState(false);
@ -66,16 +65,16 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
}, []); }, []);
const handlePress = useCallback(() => { const handlePress = useCallback(() => {
onPress(localItem.id, localItem.type); onPress(item.id, item.type);
}, [localItem.id, localItem.type, onPress]); }, [item.id, item.type, onPress]);
const handleOptionSelect = useCallback((option: string) => { const handleOptionSelect = useCallback((option: string) => {
switch (option) { switch (option) {
case 'library': case 'library':
if (localItem.inLibrary) { if (item.inLibrary) {
catalogService.removeFromLibrary(localItem.type, localItem.id); catalogService.removeFromLibrary(item.type, item.id);
} else { } else {
catalogService.addToLibrary(localItem); catalogService.addToLibrary(item);
} }
break; break;
case 'watched': case 'watched':
@ -86,27 +85,12 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
case 'share': case 'share':
break; break;
} }
}, [localItem]); }, [item]);
const handleMenuClose = useCallback(() => { const handleMenuClose = useCallback(() => {
setMenuVisible(false); setMenuVisible(false);
}, []); }, []);
useEffect(() => {
setLocalItem(initialItem);
}, [initialItem]);
useEffect(() => {
const unsubscribe = catalogService.subscribeToLibraryUpdates((libraryItems) => {
const isInLibrary = libraryItems.some(
libraryItem => libraryItem.id === localItem.id && libraryItem.type === localItem.type
);
setLocalItem(prev => ({ ...prev, inLibrary: isInLibrary }));
});
return () => unsubscribe();
}, [localItem.id, localItem.type]);
return ( return (
<> <>
<TouchableOpacity <TouchableOpacity
@ -118,12 +102,12 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
> >
<View style={styles.contentItemContainer}> <View style={styles.contentItemContainer}>
<ExpoImage <ExpoImage
source={{ uri: localItem.poster }} source={{ uri: item.poster }}
style={styles.poster} style={styles.poster}
contentFit="cover" contentFit="cover"
transition={300} transition={300}
cachePolicy="memory-disk" cachePolicy="memory-disk"
recyclingKey={`poster-${localItem.id}`} recyclingKey={`poster-${item.id}`}
onLoadStart={() => { onLoadStart={() => {
setImageLoaded(false); setImageLoaded(false);
setImageError(false); setImageError(false);
@ -148,7 +132,7 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
<MaterialIcons name="check-circle" size={22} color={currentTheme.colors.success} /> <MaterialIcons name="check-circle" size={22} color={currentTheme.colors.success} />
</View> </View>
)} )}
{localItem.inLibrary && ( {item.inLibrary && (
<View style={styles.libraryBadge}> <View style={styles.libraryBadge}>
<MaterialIcons name="bookmark" size={16} color={currentTheme.colors.white} /> <MaterialIcons name="bookmark" size={16} color={currentTheme.colors.white} />
</View> </View>
@ -159,12 +143,12 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
<DropUpMenu <DropUpMenu
visible={menuVisible} visible={menuVisible}
onClose={handleMenuClose} onClose={handleMenuClose}
item={localItem} item={item}
onOptionSelect={handleOptionSelect} onOptionSelect={handleOptionSelect}
/> />
</> </>
); );
}; });
const styles = StyleSheet.create({ const styles = StyleSheet.create({
contentItem: { contentItem: {

View file

@ -28,12 +28,16 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import { useTraktContext } from '../contexts/TraktContext'; import { useTraktContext } from '../contexts/TraktContext';
import TraktIcon from '../../assets/rating-icons/trakt.svg'; import TraktIcon from '../../assets/rating-icons/trakt.svg';
import { traktService, TraktService } from '../services/traktService'; import { traktService, TraktService, TraktImages } from '../services/traktService';
// Define interfaces for proper typing // Define interfaces for proper typing
interface LibraryItem extends StreamingContent { interface LibraryItem extends StreamingContent {
progress?: number; progress?: number;
lastWatched?: string; lastWatched?: string;
gradient: [string, string];
imdbId?: string;
traktId: number;
images?: TraktImages;
} }
interface TraktDisplayItem { interface TraktDisplayItem {
@ -47,6 +51,7 @@ interface TraktDisplayItem {
rating?: number; rating?: number;
imdbId?: string; imdbId?: string;
traktId: number; traktId: number;
images?: TraktImages;
} }
interface TraktFolder { interface TraktFolder {
@ -60,6 +65,82 @@ interface TraktFolder {
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
const TraktItem = React.memo(({ item, width, navigation, currentTheme }: { item: TraktDisplayItem; width: number; navigation: any; currentTheme: any }) => {
const [posterUrl, setPosterUrl] = useState<string | null>(null);
useEffect(() => {
let isMounted = true;
const fetchPoster = async () => {
if (item.images) {
const url = await TraktService.getTraktPosterUrlCached(item.images);
if (isMounted && url) {
setPosterUrl(url);
}
}
};
fetchPoster();
return () => { isMounted = false; };
}, [item.images]);
const handlePress = useCallback(() => {
if (item.imdbId) {
navigation.navigate('Metadata', { id: item.imdbId, type: item.type });
}
}, [navigation, item.imdbId, item.type]);
return (
<TouchableOpacity
style={[styles.itemContainer, { width }]}
onPress={handlePress}
activeOpacity={0.7}
>
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
{posterUrl ? (
<Image
source={{ uri: posterUrl }}
style={styles.poster}
contentFit="cover"
transition={300}
recyclingKey={`trakt-item-${item.id}`}
/>
) : (
<View style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, justifyContent: 'center', alignItems: 'center' }]}>
<ActivityIndicator color={currentTheme.colors.primary} />
</View>
)}
<LinearGradient
colors={['transparent', 'rgba(0,0,0,0.85)']}
style={styles.posterGradient}
>
<Text
style={[styles.itemTitle, { color: currentTheme.colors.white }]}
numberOfLines={2}
>
{item.name}
</Text>
{item.lastWatched && (
<Text style={styles.lastWatched}>
Last watched: {item.lastWatched}
</Text>
)}
{item.plays && item.plays > 1 && (
<Text style={styles.playsCount}>
{item.plays} plays
</Text>
)}
</LinearGradient>
<View style={[styles.badgeContainer, { backgroundColor: 'rgba(232,37,75,0.9)' }]}>
<TraktIcon width={12} height={12} style={{ marginRight: 4 }} />
<Text style={[styles.badgeText, { color: currentTheme.colors.white }]}>
{item.type === 'movie' ? 'Movie' : 'Series'}
</Text>
</View>
</View>
</TouchableOpacity>
);
});
const SkeletonLoader = () => { const SkeletonLoader = () => {
const pulseAnim = React.useRef(new RNAnimated.Value(0)).current; const pulseAnim = React.useRef(new RNAnimated.Value(0)).current;
const { width } = useWindowDimensions(); const { width } = useWindowDimensions();
@ -168,7 +249,7 @@ const LibraryScreen = () => {
setLoading(true); setLoading(true);
try { try {
const items = await catalogService.getLibraryItems(); const items = await catalogService.getLibraryItems();
setLibraryItems(items); setLibraryItems(items as LibraryItem[]);
} catch (error) { } catch (error) {
logger.error('Failed to load library:', error); logger.error('Failed to load library:', error);
} finally { } finally {
@ -180,7 +261,7 @@ const LibraryScreen = () => {
// Subscribe to library updates // Subscribe to library updates
const unsubscribe = catalogService.subscribeToLibraryUpdates((items) => { const unsubscribe = catalogService.subscribeToLibraryUpdates((items) => {
setLibraryItems(items); setLibraryItems(items as LibraryItem[]);
}); });
return () => { return () => {
@ -246,136 +327,6 @@ const LibraryScreen = () => {
return folders.filter(folder => folder.itemCount > 0); return folders.filter(folder => folder.itemCount > 0);
}, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]); }, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]);
// State for poster URLs (since they're now async)
const [traktPostersMap, setTraktPostersMap] = useState<Map<string, string>>(new Map());
// Prepare Trakt items with placeholders, then load posters async
const traktItems = useMemo(() => {
if (!traktAuthenticated || (!watchedMovies?.length && !watchedShows?.length)) {
return [];
}
const items: TraktDisplayItem[] = [];
// Process watched movies
if (watchedMovies) {
for (const watchedMovie of watchedMovies) {
const movie = watchedMovie.movie;
if (movie) {
const itemId = String(movie.ids.trakt);
const cachedPoster = traktPostersMap.get(itemId);
items.push({
id: itemId,
name: movie.title,
type: 'movie',
poster: cachedPoster || 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...',
year: movie.year,
lastWatched: new Date(watchedMovie.last_watched_at).toLocaleDateString(),
plays: watchedMovie.plays,
imdbId: movie.ids.imdb,
traktId: movie.ids.trakt,
});
}
}
}
// Process watched shows
if (watchedShows) {
for (const watchedShow of watchedShows) {
const show = watchedShow.show;
if (show) {
const itemId = String(show.ids.trakt);
const cachedPoster = traktPostersMap.get(itemId);
items.push({
id: itemId,
name: show.title,
type: 'series',
poster: cachedPoster || 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...',
year: show.year,
lastWatched: new Date(watchedShow.last_watched_at).toLocaleDateString(),
plays: watchedShow.plays,
imdbId: show.ids.imdb,
traktId: show.ids.trakt,
});
}
}
}
// Sort by last watched date (most recent first)
return items.sort((a, b) => {
const dateA = a.lastWatched ? new Date(a.lastWatched).getTime() : 0;
const dateB = b.lastWatched ? new Date(b.lastWatched).getTime() : 0;
return dateB - dateA;
});
}, [traktAuthenticated, watchedMovies, watchedShows, traktPostersMap]);
// Effect to load cached poster URLs
useEffect(() => {
const loadCachedPosters = async () => {
if (!traktAuthenticated) return;
const postersToLoad = new Map<string, any>();
// Collect movies that need posters
if (watchedMovies) {
for (const watchedMovie of watchedMovies) {
const movie = watchedMovie.movie;
if (movie) {
const itemId = String(movie.ids.trakt);
if (!traktPostersMap.has(itemId)) {
postersToLoad.set(itemId, movie.images);
}
}
}
}
// Collect shows that need posters
if (watchedShows) {
for (const watchedShow of watchedShows) {
const show = watchedShow.show;
if (show) {
const itemId = String(show.ids.trakt);
if (!traktPostersMap.has(itemId)) {
postersToLoad.set(itemId, show.images);
}
}
}
}
// Load posters in parallel
const posterPromises = Array.from(postersToLoad.entries()).map(async ([itemId, images]) => {
try {
const posterUrl = await TraktService.getTraktPosterUrl(images);
return {
itemId,
posterUrl: posterUrl || 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Poster'
};
} catch (error) {
logger.error(`Failed to get cached poster for ${itemId}:`, error);
return {
itemId,
posterUrl: 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Poster'
};
}
});
const results = await Promise.all(posterPromises);
// Update state with new posters
setTraktPostersMap(prevMap => {
const newMap = new Map(prevMap);
results.forEach(({ itemId, posterUrl }) => {
newMap.set(itemId, posterUrl);
});
return newMap;
});
};
loadCachedPosters();
}, [traktAuthenticated, watchedMovies, watchedShows]);
const itemWidth = (width - 48) / 2; // 2 items per row with padding const itemWidth = (width - 48) / 2; // 2 items per row with padding
const renderItem = ({ item }: { item: LibraryItem }) => ( const renderItem = ({ item }: { item: LibraryItem }) => (
@ -491,9 +442,9 @@ const LibraryScreen = () => {
<Text style={[styles.folderTitle, { color: currentTheme.colors.white }]}> <Text style={[styles.folderTitle, { color: currentTheme.colors.white }]}>
Trakt Collection Trakt Collection
</Text> </Text>
{traktAuthenticated && traktItems.length > 0 && ( {traktAuthenticated && traktFolders.length > 0 && (
<Text style={styles.folderCount}> <Text style={styles.folderCount}>
{traktItems.length} items {traktFolders.length} items
</Text> </Text>
)} )}
{!traktAuthenticated && ( {!traktAuthenticated && (
@ -514,59 +465,9 @@ const LibraryScreen = () => {
</TouchableOpacity> </TouchableOpacity>
); );
const renderTraktItem = ({ item, customWidth }: { item: TraktDisplayItem; customWidth?: number }) => { const renderTraktItem = useCallback(({ item }: { item: TraktDisplayItem }) => {
const posterUrl = item.poster || 'https://via.placeholder.com/300x450/ff0000/ffffff?text=No+Poster'; return <TraktItem item={item} width={itemWidth} navigation={navigation} currentTheme={currentTheme} />;
const width = customWidth || itemWidth; }, [itemWidth, navigation, currentTheme]);
return (
<TouchableOpacity
style={[styles.itemContainer, { width }]}
onPress={() => {
// Navigate using IMDB ID for Trakt items
if (item.imdbId) {
navigation.navigate('Metadata', { id: item.imdbId, type: item.type });
}
}}
activeOpacity={0.7}
>
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
<Image
source={{ uri: posterUrl }}
style={styles.poster}
contentFit="cover"
transition={300}
/>
<LinearGradient
colors={['transparent', 'rgba(0,0,0,0.85)']}
style={styles.posterGradient}
>
<Text
style={[styles.itemTitle, { color: currentTheme.colors.white }]}
numberOfLines={2}
>
{item.name}
</Text>
<Text style={styles.lastWatched}>
Last watched: {item.lastWatched}
</Text>
{item.plays && item.plays > 1 && (
<Text style={styles.playsCount}>
{item.plays} plays
</Text>
)}
</LinearGradient>
{/* Trakt badge */}
<View style={[styles.badgeContainer, { backgroundColor: 'rgba(232,37,75,0.9)' }]}>
<TraktIcon width={12} height={12} style={{ marginRight: 4 }} />
<Text style={[styles.badgeText, { color: currentTheme.colors.white }]}>
{item.type === 'movie' ? 'Movie' : 'Series'}
</Text>
</View>
</View>
</TouchableOpacity>
);
};
// Get items for a specific Trakt folder // Get items for a specific Trakt folder
const getTraktFolderItems = useCallback((folderId: string): TraktDisplayItem[] => { const getTraktFolderItems = useCallback((folderId: string): TraktDisplayItem[] => {
@ -579,19 +480,17 @@ const LibraryScreen = () => {
for (const watchedMovie of watchedMovies) { for (const watchedMovie of watchedMovies) {
const movie = watchedMovie.movie; const movie = watchedMovie.movie;
if (movie) { if (movie) {
const itemId = String(movie.ids.trakt);
const cachedPoster = traktPostersMap.get(itemId);
items.push({ items.push({
id: itemId, id: String(movie.ids.trakt),
name: movie.title, name: movie.title,
type: 'movie', type: 'movie',
poster: cachedPoster || 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', poster: 'placeholder',
year: movie.year, year: movie.year,
lastWatched: new Date(watchedMovie.last_watched_at).toLocaleDateString(), lastWatched: new Date(watchedMovie.last_watched_at).toLocaleDateString(),
plays: watchedMovie.plays, plays: watchedMovie.plays,
imdbId: movie.ids.imdb, imdbId: movie.ids.imdb,
traktId: movie.ids.trakt, traktId: movie.ids.trakt,
images: movie.images,
}); });
} }
} }
@ -601,19 +500,17 @@ const LibraryScreen = () => {
for (const watchedShow of watchedShows) { for (const watchedShow of watchedShows) {
const show = watchedShow.show; const show = watchedShow.show;
if (show) { if (show) {
const itemId = String(show.ids.trakt);
const cachedPoster = traktPostersMap.get(itemId);
items.push({ items.push({
id: itemId, id: String(show.ids.trakt),
name: show.title, name: show.title,
type: 'series', type: 'series',
poster: cachedPoster || 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', poster: 'placeholder',
year: show.year, year: show.year,
lastWatched: new Date(watchedShow.last_watched_at).toLocaleDateString(), lastWatched: new Date(watchedShow.last_watched_at).toLocaleDateString(),
plays: watchedShow.plays, plays: watchedShow.plays,
imdbId: show.ids.imdb, imdbId: show.ids.imdb,
traktId: show.ids.trakt, traktId: show.ids.trakt,
images: show.images,
}); });
} }
} }
@ -625,32 +522,28 @@ const LibraryScreen = () => {
if (continueWatching) { if (continueWatching) {
for (const item of continueWatching) { for (const item of continueWatching) {
if (item.type === 'movie' && item.movie) { if (item.type === 'movie' && item.movie) {
const itemId = String(item.movie.ids.trakt);
const cachedPoster = traktPostersMap.get(itemId);
items.push({ items.push({
id: itemId, id: String(item.movie.ids.trakt),
name: item.movie.title, name: item.movie.title,
type: 'movie', type: 'movie',
poster: cachedPoster || 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', poster: 'placeholder',
year: item.movie.year, year: item.movie.year,
lastWatched: new Date(item.paused_at).toLocaleDateString(), lastWatched: new Date(item.paused_at).toLocaleDateString(),
imdbId: item.movie.ids.imdb, imdbId: item.movie.ids.imdb,
traktId: item.movie.ids.trakt, traktId: item.movie.ids.trakt,
images: item.movie.images,
}); });
} else if (item.type === 'episode' && item.show && item.episode) { } else if (item.type === 'episode' && item.show && item.episode) {
const itemId = String(item.show.ids.trakt);
const cachedPoster = traktPostersMap.get(itemId);
items.push({ items.push({
id: `${item.show.ids.trakt}:${item.episode.season}:${item.episode.number}`, id: `${item.show.ids.trakt}:${item.episode.season}:${item.episode.number}`,
name: `${item.show.title} S${item.episode.season}E${item.episode.number}`, name: `${item.show.title} S${item.episode.season}E${item.episode.number}`,
type: 'series', type: 'series',
poster: cachedPoster || 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...', poster: 'placeholder',
year: item.show.year, year: item.show.year,
lastWatched: new Date(item.paused_at).toLocaleDateString(), lastWatched: new Date(item.paused_at).toLocaleDateString(),
imdbId: item.show.ids.imdb, imdbId: item.show.ids.imdb,
traktId: item.show.ids.trakt, traktId: item.show.ids.trakt,
images: item.show.images,
}); });
} }
} }
@ -663,19 +556,16 @@ const LibraryScreen = () => {
for (const watchlistMovie of watchlistMovies) { for (const watchlistMovie of watchlistMovies) {
const movie = watchlistMovie.movie; const movie = watchlistMovie.movie;
if (movie) { if (movie) {
const itemId = String(movie.ids.trakt);
const posterUrl = TraktService.getTraktPosterUrl(movie.images) ||
'https://via.placeholder.com/300x450/cccccc/666666?text=No+Poster';
items.push({ items.push({
id: itemId, id: String(movie.ids.trakt),
name: movie.title, name: movie.title,
type: 'movie', type: 'movie',
poster: posterUrl, poster: 'placeholder',
year: movie.year, year: movie.year,
lastWatched: new Date(watchlistMovie.listed_at).toLocaleDateString(), lastWatched: new Date(watchlistMovie.listed_at).toLocaleDateString(),
imdbId: movie.ids.imdb, imdbId: movie.ids.imdb,
traktId: movie.ids.trakt, traktId: movie.ids.trakt,
images: movie.images,
}); });
} }
} }
@ -685,19 +575,16 @@ const LibraryScreen = () => {
for (const watchlistShow of watchlistShows) { for (const watchlistShow of watchlistShows) {
const show = watchlistShow.show; const show = watchlistShow.show;
if (show) { if (show) {
const itemId = String(show.ids.trakt);
const posterUrl = TraktService.getTraktPosterUrl(show.images) ||
'https://via.placeholder.com/300x450/cccccc/666666?text=No+Poster';
items.push({ items.push({
id: itemId, id: String(show.ids.trakt),
name: show.title, name: show.title,
type: 'series', type: 'series',
poster: posterUrl, poster: 'placeholder',
year: show.year, year: show.year,
lastWatched: new Date(watchlistShow.listed_at).toLocaleDateString(), lastWatched: new Date(watchlistShow.listed_at).toLocaleDateString(),
imdbId: show.ids.imdb, imdbId: show.ids.imdb,
traktId: show.ids.trakt, traktId: show.ids.trakt,
images: show.images,
}); });
} }
} }
@ -710,19 +597,16 @@ const LibraryScreen = () => {
for (const collectionMovie of collectionMovies) { for (const collectionMovie of collectionMovies) {
const movie = collectionMovie.movie; const movie = collectionMovie.movie;
if (movie) { if (movie) {
const itemId = String(movie.ids.trakt);
const posterUrl = TraktService.getTraktPosterUrl(movie.images) ||
'https://via.placeholder.com/300x450/cccccc/666666?text=No+Poster';
items.push({ items.push({
id: itemId, id: String(movie.ids.trakt),
name: movie.title, name: movie.title,
type: 'movie', type: 'movie',
poster: posterUrl, poster: 'placeholder',
year: movie.year, year: movie.year,
lastWatched: new Date(collectionMovie.collected_at).toLocaleDateString(), lastWatched: new Date(collectionMovie.collected_at).toLocaleDateString(),
imdbId: movie.ids.imdb, imdbId: movie.ids.imdb,
traktId: movie.ids.trakt, traktId: movie.ids.trakt,
images: movie.images,
}); });
} }
} }
@ -732,19 +616,16 @@ const LibraryScreen = () => {
for (const collectionShow of collectionShows) { for (const collectionShow of collectionShows) {
const show = collectionShow.show; const show = collectionShow.show;
if (show) { if (show) {
const itemId = String(show.ids.trakt);
const posterUrl = TraktService.getTraktPosterUrl(show.images) ||
'https://via.placeholder.com/300x450/cccccc/666666?text=No+Poster';
items.push({ items.push({
id: itemId, id: String(show.ids.trakt),
name: show.title, name: show.title,
type: 'series', type: 'series',
poster: posterUrl, poster: 'placeholder',
year: show.year, year: show.year,
lastWatched: new Date(collectionShow.collected_at).toLocaleDateString(), lastWatched: new Date(collectionShow.collected_at).toLocaleDateString(),
imdbId: show.ids.imdb, imdbId: show.ids.imdb,
traktId: show.ids.trakt, traktId: show.ids.trakt,
images: show.images,
}); });
} }
} }
@ -757,37 +638,31 @@ const LibraryScreen = () => {
for (const ratedItem of ratedContent) { for (const ratedItem of ratedContent) {
if (ratedItem.movie) { if (ratedItem.movie) {
const movie = ratedItem.movie; const movie = ratedItem.movie;
const itemId = String(movie.ids.trakt);
const posterUrl = TraktService.getTraktPosterUrl(movie.images) ||
'https://via.placeholder.com/300x450/cccccc/666666?text=No+Poster';
items.push({ items.push({
id: itemId, id: String(movie.ids.trakt),
name: movie.title, name: movie.title,
type: 'movie', type: 'movie',
poster: posterUrl, poster: 'placeholder',
year: movie.year, year: movie.year,
lastWatched: new Date(ratedItem.rated_at).toLocaleDateString(), lastWatched: new Date(ratedItem.rated_at).toLocaleDateString(),
rating: ratedItem.rating, rating: ratedItem.rating,
imdbId: movie.ids.imdb, imdbId: movie.ids.imdb,
traktId: movie.ids.trakt, traktId: movie.ids.trakt,
images: movie.images,
}); });
} else if (ratedItem.show) { } else if (ratedItem.show) {
const show = ratedItem.show; const show = ratedItem.show;
const itemId = String(show.ids.trakt);
const posterUrl = TraktService.getTraktPosterUrl(show.images) ||
'https://via.placeholder.com/300x450/cccccc/666666?text=No+Poster';
items.push({ items.push({
id: itemId, id: String(show.ids.trakt),
name: show.title, name: show.title,
type: 'series', type: 'series',
poster: posterUrl, poster: 'placeholder',
year: show.year, year: show.year,
lastWatched: new Date(ratedItem.rated_at).toLocaleDateString(), lastWatched: new Date(ratedItem.rated_at).toLocaleDateString(),
rating: ratedItem.rating, rating: ratedItem.rating,
imdbId: show.ids.imdb, imdbId: show.ids.imdb,
traktId: show.ids.trakt, traktId: show.ids.trakt,
images: show.images,
}); });
} }
} }
@ -801,7 +676,7 @@ const LibraryScreen = () => {
const dateB = b.lastWatched ? new Date(b.lastWatched).getTime() : 0; const dateB = b.lastWatched ? new Date(b.lastWatched).getTime() : 0;
return dateB - dateA; return dateB - dateA;
}); });
}, [watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent, traktPostersMap]); }, [watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]);
const renderTraktContent = () => { const renderTraktContent = () => {
if (traktLoading) { if (traktLoading) {
@ -880,70 +755,21 @@ const LibraryScreen = () => {
); );
} }
// Separate movies and shows for the selected folder
const movies = folderItems.filter(item => item.type === 'movie');
const shows = folderItems.filter(item => item.type === 'series');
return ( return (
<ScrollView <FlatList
style={styles.sectionsContainer} data={folderItems}
renderItem={({ item }) => renderTraktItem({ item })}
keyExtractor={(item) => `${item.type}-${item.id}`}
numColumns={2}
columnWrapperStyle={styles.row}
style={styles.traktContainer}
contentContainerStyle={{ paddingBottom: insets.bottom + 80 }}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={styles.sectionsContent} initialNumToRender={10}
> maxToRenderPerBatch={10}
{movies.length > 0 && ( windowSize={21}
<View style={styles.section}> removeClippedSubviews={Platform.OS === 'android'}
<View style={styles.sectionHeader}> />
<MaterialIcons
name="movie"
size={24}
color={currentTheme.colors.white}
style={styles.sectionIcon}
/>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.white }]}>
Movies ({movies.length})
</Text>
</View>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.horizontalScrollContent}
>
{movies.map((item) => (
<View key={item.id} style={{ width: itemWidth * 0.8, marginRight: 12 }}>
{renderTraktItem({ item, customWidth: itemWidth * 0.8 })}
</View>
))}
</ScrollView>
</View>
)}
{shows.length > 0 && (
<View style={styles.section}>
<View style={styles.sectionHeader}>
<MaterialIcons
name="live-tv"
size={24}
color={currentTheme.colors.white}
style={styles.sectionIcon}
/>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.white }]}>
TV Shows ({shows.length})
</Text>
</View>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.horizontalScrollContent}
>
{shows.map((item) => (
<View key={item.id} style={{ width: itemWidth * 0.8, marginRight: 12 }}>
{renderTraktItem({ item, customWidth: itemWidth * 0.8 })}
</View>
))}
</ScrollView>
</View>
)}
</ScrollView>
); );
}; };
@ -1387,6 +1213,17 @@ const styles = StyleSheet.create({
headerSpacer: { headerSpacer: {
width: 44, // Match the back button width width: 44, // Match the back button width
}, },
traktContainer: {
flex: 1,
},
emptyListText: {
fontSize: 16,
fontWeight: '500',
},
row: {
justifyContent: 'space-between',
paddingHorizontal: 16,
},
}); });
export default LibraryScreen; export default LibraryScreen;