diff --git a/src/components/home/ContentItem.tsx b/src/components/home/ContentItem.tsx
index 860a3012..0483bc27 100644
--- a/src/components/home/ContentItem.tsx
+++ b/src/components/home/ContentItem.tsx
@@ -4,7 +4,7 @@ import { Image as ExpoImage } from 'expo-image';
import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../../contexts/ThemeContext';
import { catalogService, StreamingContent } from '../../services/catalogService';
-import DropUpMenu from './DropUpMenu';
+import { DropUpMenu } from './DropUpMenu';
interface ContentItemProps {
item: StreamingContent;
@@ -53,9 +53,8 @@ const calculatePosterLayout = (screenWidth: number) => {
const posterLayout = calculatePosterLayout(width);
const POSTER_WIDTH = posterLayout.posterWidth;
-const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
+const ContentItem = React.memo(({ item, onPress }: ContentItemProps) => {
const [menuVisible, setMenuVisible] = useState(false);
- const [localItem, setLocalItem] = useState(initialItem);
const [isWatched, setIsWatched] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
const [imageError, setImageError] = useState(false);
@@ -66,16 +65,16 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
}, []);
const handlePress = useCallback(() => {
- onPress(localItem.id, localItem.type);
- }, [localItem.id, localItem.type, onPress]);
+ onPress(item.id, item.type);
+ }, [item.id, item.type, onPress]);
const handleOptionSelect = useCallback((option: string) => {
switch (option) {
case 'library':
- if (localItem.inLibrary) {
- catalogService.removeFromLibrary(localItem.type, localItem.id);
+ if (item.inLibrary) {
+ catalogService.removeFromLibrary(item.type, item.id);
} else {
- catalogService.addToLibrary(localItem);
+ catalogService.addToLibrary(item);
}
break;
case 'watched':
@@ -86,27 +85,12 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
case 'share':
break;
}
- }, [localItem]);
+ }, [item]);
const handleMenuClose = useCallback(() => {
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 (
<>
{
>
{
setImageLoaded(false);
setImageError(false);
@@ -148,7 +132,7 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
)}
- {localItem.inLibrary && (
+ {item.inLibrary && (
@@ -159,12 +143,12 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
>
);
-};
+});
const styles = StyleSheet.create({
contentItem: {
diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx
index 22f61a29..ef80caff 100644
--- a/src/screens/LibraryScreen.tsx
+++ b/src/screens/LibraryScreen.tsx
@@ -28,12 +28,16 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../contexts/ThemeContext';
import { useTraktContext } from '../contexts/TraktContext';
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
interface LibraryItem extends StreamingContent {
progress?: number;
lastWatched?: string;
+ gradient: [string, string];
+ imdbId?: string;
+ traktId: number;
+ images?: TraktImages;
}
interface TraktDisplayItem {
@@ -47,6 +51,7 @@ interface TraktDisplayItem {
rating?: number;
imdbId?: string;
traktId: number;
+ images?: TraktImages;
}
interface TraktFolder {
@@ -60,6 +65,82 @@ interface TraktFolder {
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(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 (
+
+
+ {posterUrl ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ {item.name}
+
+ {item.lastWatched && (
+
+ Last watched: {item.lastWatched}
+
+ )}
+ {item.plays && item.plays > 1 && (
+
+ {item.plays} plays
+
+ )}
+
+
+
+
+
+ {item.type === 'movie' ? 'Movie' : 'Series'}
+
+
+
+
+ );
+});
+
const SkeletonLoader = () => {
const pulseAnim = React.useRef(new RNAnimated.Value(0)).current;
const { width } = useWindowDimensions();
@@ -168,7 +249,7 @@ const LibraryScreen = () => {
setLoading(true);
try {
const items = await catalogService.getLibraryItems();
- setLibraryItems(items);
+ setLibraryItems(items as LibraryItem[]);
} catch (error) {
logger.error('Failed to load library:', error);
} finally {
@@ -180,7 +261,7 @@ const LibraryScreen = () => {
// Subscribe to library updates
const unsubscribe = catalogService.subscribeToLibraryUpdates((items) => {
- setLibraryItems(items);
+ setLibraryItems(items as LibraryItem[]);
});
return () => {
@@ -246,136 +327,6 @@ const LibraryScreen = () => {
return folders.filter(folder => folder.itemCount > 0);
}, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]);
- // State for poster URLs (since they're now async)
- const [traktPostersMap, setTraktPostersMap] = useState
);
- const renderTraktItem = ({ item, customWidth }: { item: TraktDisplayItem; customWidth?: number }) => {
- const posterUrl = item.poster || 'https://via.placeholder.com/300x450/ff0000/ffffff?text=No+Poster';
- const width = customWidth || itemWidth;
-
- return (
- {
- // Navigate using IMDB ID for Trakt items
- if (item.imdbId) {
- navigation.navigate('Metadata', { id: item.imdbId, type: item.type });
- }
- }}
- activeOpacity={0.7}
- >
-
-
-
-
- {item.name}
-
-
- Last watched: {item.lastWatched}
-
- {item.plays && item.plays > 1 && (
-
- {item.plays} plays
-
- )}
-
-
- {/* Trakt badge */}
-
-
-
- {item.type === 'movie' ? 'Movie' : 'Series'}
-
-
-
-
- );
- };
+ const renderTraktItem = useCallback(({ item }: { item: TraktDisplayItem }) => {
+ return ;
+ }, [itemWidth, navigation, currentTheme]);
// Get items for a specific Trakt folder
const getTraktFolderItems = useCallback((folderId: string): TraktDisplayItem[] => {
@@ -579,19 +480,17 @@ const LibraryScreen = () => {
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,
+ id: String(movie.ids.trakt),
name: movie.title,
type: 'movie',
- poster: cachedPoster || 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...',
+ poster: 'placeholder',
year: movie.year,
lastWatched: new Date(watchedMovie.last_watched_at).toLocaleDateString(),
plays: watchedMovie.plays,
imdbId: movie.ids.imdb,
traktId: movie.ids.trakt,
+ images: movie.images,
});
}
}
@@ -601,19 +500,17 @@ const LibraryScreen = () => {
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,
+ id: String(show.ids.trakt),
name: show.title,
type: 'series',
- poster: cachedPoster || 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...',
+ poster: 'placeholder',
year: show.year,
lastWatched: new Date(watchedShow.last_watched_at).toLocaleDateString(),
plays: watchedShow.plays,
imdbId: show.ids.imdb,
traktId: show.ids.trakt,
+ images: show.images,
});
}
}
@@ -625,32 +522,28 @@ const LibraryScreen = () => {
if (continueWatching) {
for (const item of continueWatching) {
if (item.type === 'movie' && item.movie) {
- const itemId = String(item.movie.ids.trakt);
- const cachedPoster = traktPostersMap.get(itemId);
-
items.push({
- id: itemId,
+ id: String(item.movie.ids.trakt),
name: item.movie.title,
type: 'movie',
- poster: cachedPoster || 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...',
+ poster: 'placeholder',
year: item.movie.year,
lastWatched: new Date(item.paused_at).toLocaleDateString(),
imdbId: item.movie.ids.imdb,
traktId: item.movie.ids.trakt,
+ images: item.movie.images,
});
} else if (item.type === 'episode' && item.show && item.episode) {
- const itemId = String(item.show.ids.trakt);
- const cachedPoster = traktPostersMap.get(itemId);
-
items.push({
id: `${item.show.ids.trakt}:${item.episode.season}:${item.episode.number}`,
name: `${item.show.title} S${item.episode.season}E${item.episode.number}`,
type: 'series',
- poster: cachedPoster || 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...',
+ poster: 'placeholder',
year: item.show.year,
lastWatched: new Date(item.paused_at).toLocaleDateString(),
imdbId: item.show.ids.imdb,
traktId: item.show.ids.trakt,
+ images: item.show.images,
});
}
}
@@ -663,19 +556,16 @@ const LibraryScreen = () => {
for (const watchlistMovie of watchlistMovies) {
const movie = watchlistMovie.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({
- id: itemId,
+ id: String(movie.ids.trakt),
name: movie.title,
type: 'movie',
- poster: posterUrl,
+ poster: 'placeholder',
year: movie.year,
lastWatched: new Date(watchlistMovie.listed_at).toLocaleDateString(),
imdbId: movie.ids.imdb,
traktId: movie.ids.trakt,
+ images: movie.images,
});
}
}
@@ -685,19 +575,16 @@ const LibraryScreen = () => {
for (const watchlistShow of watchlistShows) {
const show = watchlistShow.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({
- id: itemId,
+ id: String(show.ids.trakt),
name: show.title,
type: 'series',
- poster: posterUrl,
+ poster: 'placeholder',
year: show.year,
lastWatched: new Date(watchlistShow.listed_at).toLocaleDateString(),
imdbId: show.ids.imdb,
traktId: show.ids.trakt,
+ images: show.images,
});
}
}
@@ -710,19 +597,16 @@ const LibraryScreen = () => {
for (const collectionMovie of collectionMovies) {
const movie = collectionMovie.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({
- id: itemId,
+ id: String(movie.ids.trakt),
name: movie.title,
type: 'movie',
- poster: posterUrl,
+ poster: 'placeholder',
year: movie.year,
lastWatched: new Date(collectionMovie.collected_at).toLocaleDateString(),
imdbId: movie.ids.imdb,
traktId: movie.ids.trakt,
+ images: movie.images,
});
}
}
@@ -732,19 +616,16 @@ const LibraryScreen = () => {
for (const collectionShow of collectionShows) {
const show = collectionShow.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({
- id: itemId,
+ id: String(show.ids.trakt),
name: show.title,
type: 'series',
- poster: posterUrl,
+ poster: 'placeholder',
year: show.year,
lastWatched: new Date(collectionShow.collected_at).toLocaleDateString(),
imdbId: show.ids.imdb,
traktId: show.ids.trakt,
+ images: show.images,
});
}
}
@@ -757,37 +638,31 @@ const LibraryScreen = () => {
for (const ratedItem of ratedContent) {
if (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({
- id: itemId,
+ id: String(movie.ids.trakt),
name: movie.title,
type: 'movie',
- poster: posterUrl,
+ poster: 'placeholder',
year: movie.year,
lastWatched: new Date(ratedItem.rated_at).toLocaleDateString(),
rating: ratedItem.rating,
imdbId: movie.ids.imdb,
traktId: movie.ids.trakt,
+ images: movie.images,
});
} else if (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({
- id: itemId,
+ id: String(show.ids.trakt),
name: show.title,
type: 'series',
- poster: posterUrl,
+ poster: 'placeholder',
year: show.year,
lastWatched: new Date(ratedItem.rated_at).toLocaleDateString(),
rating: ratedItem.rating,
imdbId: show.ids.imdb,
traktId: show.ids.trakt,
+ images: show.images,
});
}
}
@@ -801,7 +676,7 @@ const LibraryScreen = () => {
const dateB = b.lastWatched ? new Date(b.lastWatched).getTime() : 0;
return dateB - dateA;
});
- }, [watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent, traktPostersMap]);
+ }, [watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]);
const renderTraktContent = () => {
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 (
- renderTraktItem({ item })}
+ keyExtractor={(item) => `${item.type}-${item.id}`}
+ numColumns={2}
+ columnWrapperStyle={styles.row}
+ style={styles.traktContainer}
+ contentContainerStyle={{ paddingBottom: insets.bottom + 80 }}
showsVerticalScrollIndicator={false}
- contentContainerStyle={styles.sectionsContent}
- >
- {movies.length > 0 && (
-
-
-
-
- Movies ({movies.length})
-
-
-
- {movies.map((item) => (
-
- {renderTraktItem({ item, customWidth: itemWidth * 0.8 })}
-
- ))}
-
-
- )}
-
- {shows.length > 0 && (
-
-
-
-
- TV Shows ({shows.length})
-
-
-
- {shows.map((item) => (
-
- {renderTraktItem({ item, customWidth: itemWidth * 0.8 })}
-
- ))}
-
-
- )}
-
+ initialNumToRender={10}
+ maxToRenderPerBatch={10}
+ windowSize={21}
+ removeClippedSubviews={Platform.OS === 'android'}
+ />
);
};
@@ -1387,6 +1213,17 @@ const styles = StyleSheet.create({
headerSpacer: {
width: 44, // Match the back button width
},
+ traktContainer: {
+ flex: 1,
+ },
+ emptyListText: {
+ fontSize: 16,
+ fontWeight: '500',
+ },
+ row: {
+ justifyContent: 'space-between',
+ paddingHorizontal: 16,
+ },
});
export default LibraryScreen;
\ No newline at end of file