diff --git a/src/components/home/CatalogSection.tsx b/src/components/home/CatalogSection.tsx
index 71937b0e..80a3bdc2 100644
--- a/src/components/home/CatalogSection.tsx
+++ b/src/components/home/CatalogSection.tsx
@@ -15,26 +15,40 @@ interface CatalogSectionProps {
const { width } = Dimensions.get('window');
-// Dynamic poster calculation based on screen width
+// Dynamic poster calculation based on screen width - show 1/4 of next poster
const calculatePosterLayout = (screenWidth: number) => {
- const MIN_POSTER_WIDTH = 110; // Minimum poster width for readability
- const MAX_POSTER_WIDTH = 140; // Maximum poster width to prevent oversized posters
- const HORIZONTAL_PADDING = 50; // Total horizontal padding/margins
+ const MIN_POSTER_WIDTH = 100; // Reduced minimum for more posters
+ const MAX_POSTER_WIDTH = 130; // Reduced maximum for more posters
+ const LEFT_PADDING = 16; // Left padding
+ const SPACING = 8; // Space between posters
- // Calculate how many posters can fit
- const availableWidth = screenWidth - HORIZONTAL_PADDING;
- const maxColumns = Math.floor(availableWidth / MIN_POSTER_WIDTH);
+ // Calculate available width for posters (reserve space for left padding)
+ const availableWidth = screenWidth - LEFT_PADDING;
- // Limit to reasonable number of columns (3-6)
- const numColumns = Math.min(Math.max(maxColumns, 3), 6);
+ // Try different numbers of full posters to find the best fit
+ let bestLayout = { numFullPosters: 3, posterWidth: 120 };
- // Calculate actual poster width
- const posterWidth = Math.min(availableWidth / numColumns, MAX_POSTER_WIDTH);
+ for (let n = 3; n <= 6; n++) {
+ // Calculate poster width needed for N full posters + 0.25 partial poster
+ // Formula: N * posterWidth + (N-1) * spacing + 0.25 * posterWidth = availableWidth - rightPadding
+ // Simplified: posterWidth * (N + 0.25) + (N-1) * spacing = availableWidth - rightPadding
+ // We'll use minimal right padding (8px) to maximize space
+ const usableWidth = availableWidth - 8;
+ const posterWidth = (usableWidth - (n - 1) * SPACING) / (n + 0.25);
+
+ console.log(`[CatalogSection] Testing ${n} posters: width=${posterWidth.toFixed(1)}px, screen=${screenWidth}px`);
+
+ if (posterWidth >= MIN_POSTER_WIDTH && posterWidth <= MAX_POSTER_WIDTH) {
+ bestLayout = { numFullPosters: n, posterWidth };
+ console.log(`[CatalogSection] Selected layout: ${n} full posters at ${posterWidth.toFixed(1)}px each`);
+ }
+ }
return {
- numColumns,
- posterWidth,
- spacing: 12 // Space between posters
+ numFullPosters: bestLayout.numFullPosters,
+ posterWidth: bestLayout.posterWidth,
+ spacing: SPACING,
+ partialPosterWidth: bestLayout.posterWidth * 0.25 // 1/4 of next poster
};
};
@@ -98,18 +112,18 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => {
keyExtractor={(item) => `${item.id}-${item.type}`}
horizontal
showsHorizontalScrollIndicator={false}
- contentContainerStyle={styles.catalogList}
- snapToInterval={POSTER_WIDTH + 12}
+ contentContainerStyle={[styles.catalogList, { paddingRight: 16 - posterLayout.partialPosterWidth }]}
+ snapToInterval={POSTER_WIDTH + 8}
decelerationRate="fast"
snapToAlignment="start"
- ItemSeparatorComponent={() => }
+ ItemSeparatorComponent={() => }
initialNumToRender={4}
maxToRenderPerBatch={4}
windowSize={5}
removeClippedSubviews={Platform.OS === 'android'}
getItemLayout={(data, index) => ({
- length: POSTER_WIDTH + 12,
- offset: (POSTER_WIDTH + 12) * index,
+ length: POSTER_WIDTH + 8,
+ offset: (POSTER_WIDTH + 8) * index,
index,
})}
/>
diff --git a/src/components/home/ContentItem.tsx b/src/components/home/ContentItem.tsx
index f5061c54..860a3012 100644
--- a/src/components/home/ContentItem.tsx
+++ b/src/components/home/ContentItem.tsx
@@ -13,26 +13,40 @@ interface ContentItemProps {
const { width } = Dimensions.get('window');
-// Dynamic poster calculation based on screen width
+// Dynamic poster calculation based on screen width - show 1/4 of next poster
const calculatePosterLayout = (screenWidth: number) => {
- const MIN_POSTER_WIDTH = 110; // Minimum poster width for readability
- const MAX_POSTER_WIDTH = 140; // Maximum poster width to prevent oversized posters
- const HORIZONTAL_PADDING = 50; // Total horizontal padding/margins
+ const MIN_POSTER_WIDTH = 100; // Reduced minimum for more posters
+ const MAX_POSTER_WIDTH = 130; // Reduced maximum for more posters
+ const LEFT_PADDING = 16; // Left padding
+ const SPACING = 8; // Space between posters
- // Calculate how many posters can fit
- const availableWidth = screenWidth - HORIZONTAL_PADDING;
- const maxColumns = Math.floor(availableWidth / MIN_POSTER_WIDTH);
+ // Calculate available width for posters (reserve space for left padding)
+ const availableWidth = screenWidth - LEFT_PADDING;
- // Limit to reasonable number of columns (3-6)
- const numColumns = Math.min(Math.max(maxColumns, 3), 6);
+ // Try different numbers of full posters to find the best fit
+ let bestLayout = { numFullPosters: 3, posterWidth: 120 };
- // Calculate actual poster width
- const posterWidth = Math.min(availableWidth / numColumns, MAX_POSTER_WIDTH);
+ for (let n = 3; n <= 6; n++) {
+ // Calculate poster width needed for N full posters + 0.25 partial poster
+ // Formula: N * posterWidth + (N-1) * spacing + 0.25 * posterWidth = availableWidth - rightPadding
+ // Simplified: posterWidth * (N + 0.25) + (N-1) * spacing = availableWidth - rightPadding
+ // We'll use minimal right padding (8px) to maximize space
+ const usableWidth = availableWidth - 8;
+ const posterWidth = (usableWidth - (n - 1) * SPACING) / (n + 0.25);
+
+ console.log(`[ContentItem] Testing ${n} posters: width=${posterWidth.toFixed(1)}px, screen=${screenWidth}px`);
+
+ if (posterWidth >= MIN_POSTER_WIDTH && posterWidth <= MAX_POSTER_WIDTH) {
+ bestLayout = { numFullPosters: n, posterWidth };
+ console.log(`[ContentItem] Selected layout: ${n} full posters at ${posterWidth.toFixed(1)}px each`);
+ }
+ }
return {
- numColumns,
- posterWidth,
- spacing: 12 // Space between posters
+ numFullPosters: bestLayout.numFullPosters,
+ posterWidth: bestLayout.posterWidth,
+ spacing: SPACING,
+ partialPosterWidth: bestLayout.posterWidth * 0.25 // 1/4 of next poster
};
};
@@ -157,28 +171,28 @@ const styles = StyleSheet.create({
width: POSTER_WIDTH,
aspectRatio: 2/3,
margin: 0,
- borderRadius: 8,
+ borderRadius: 4,
overflow: 'hidden',
position: 'relative',
- elevation: 8,
+ elevation: 6,
shadowColor: '#000',
- shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.3,
- shadowRadius: 8,
- borderWidth: 1,
- borderColor: 'rgba(255,255,255,0.08)',
+ shadowOffset: { width: 0, height: 3 },
+ shadowOpacity: 0.25,
+ shadowRadius: 6,
+ borderWidth: 0.5,
+ borderColor: 'rgba(255,255,255,0.12)',
},
contentItemContainer: {
width: '100%',
height: '100%',
- borderRadius: 8,
+ borderRadius: 4,
overflow: 'hidden',
position: 'relative',
},
poster: {
width: '100%',
height: '100%',
- borderRadius: 8,
+ borderRadius: 4,
},
loadingOverlay: {
position: 'absolute',
diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx
index 28d92fa6..483f5122 100644
--- a/src/screens/HomeScreen.tsx
+++ b/src/screens/HomeScreen.tsx
@@ -22,6 +22,7 @@ import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator';
import { StreamingContent, CatalogContent, catalogService } from '../services/catalogService';
+import { stremioService } from '../services/stremioService';
import { Stream } from '../types/metadata';
import { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
@@ -386,16 +387,16 @@ const HomeScreen = () => {
const { currentTheme } = useTheme();
const continueWatchingRef = useRef(null);
const { settings } = useSettings();
+ const { lastUpdate } = useCatalogContext(); // Add catalog context to listen for addon changes
const [showHeroSection, setShowHeroSection] = useState(settings.showHeroSection);
const [featuredContentSource, setFeaturedContentSource] = useState(settings.featuredContentSource);
const refreshTimeoutRef = useRef(null);
const [hasContinueWatching, setHasContinueWatching] = useState(false);
- const {
- catalogs,
- loading: catalogsLoading,
- refreshCatalogs
- } = useHomeCatalogs();
+ const [catalogs, setCatalogs] = useState([]);
+ const [catalogsLoading, setCatalogsLoading] = useState(true);
+ const [loadedCatalogCount, setLoadedCatalogCount] = useState(0);
+ const totalCatalogsRef = useRef(0);
const {
featuredContent,
@@ -405,10 +406,116 @@ const HomeScreen = () => {
refreshFeatured
} = useFeaturedContent();
+ // Progressive catalog loading function
+ const loadCatalogsProgressively = useCallback(async () => {
+ setCatalogsLoading(true);
+ setCatalogs([]);
+ setLoadedCatalogCount(0);
+
+ try {
+ const addons = await catalogService.getAllAddons();
+
+ // Create placeholder array with proper order and track indices
+ const catalogPlaceholders: (CatalogContent | null)[] = [];
+ const catalogPromises: Promise[] = [];
+ let catalogIndex = 0;
+
+ for (const addon of addons) {
+ if (addon.catalogs) {
+ for (const catalog of addon.catalogs) {
+ const currentIndex = catalogIndex;
+ catalogPlaceholders.push(null); // Reserve position
+
+ const catalogPromise = (async () => {
+ try {
+ const addonManifest = await stremioService.getInstalledAddonsAsync();
+ const manifest = addonManifest.find((a: any) => a.id === addon.id);
+ if (!manifest) return;
+
+ const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1);
+ if (metas && metas.length > 0) {
+ const items = metas.map((meta: any) => ({
+ id: meta.id,
+ type: meta.type,
+ name: meta.name,
+ poster: meta.poster,
+ posterShape: meta.posterShape,
+ banner: meta.background,
+ logo: meta.logo,
+ imdbRating: meta.imdbRating,
+ year: meta.year,
+ genres: meta.genres,
+ description: meta.description,
+ runtime: meta.runtime,
+ released: meta.released,
+ trailerStreams: meta.trailerStreams,
+ videos: meta.videos,
+ directors: meta.director,
+ creators: meta.creator,
+ certification: meta.certification
+ }));
+
+ let displayName = catalog.name;
+ const contentType = catalog.type === 'movie' ? 'Movies' : 'TV Shows';
+ if (!displayName.toLowerCase().includes(contentType.toLowerCase())) {
+ displayName = `${displayName} ${contentType}`;
+ }
+
+ const catalogContent = {
+ addon: addon.id,
+ type: catalog.type,
+ id: catalog.id,
+ name: displayName,
+ items
+ };
+
+ console.log(`[HomeScreen] Loaded catalog: ${displayName} at position ${currentIndex} (${items.length} items)`);
+
+ // Update the catalog at its specific position
+ setCatalogs(prevCatalogs => {
+ const newCatalogs = [...prevCatalogs];
+ newCatalogs[currentIndex] = catalogContent;
+ return newCatalogs;
+ });
+ }
+ } catch (error) {
+ console.error(`[HomeScreen] Failed to load ${catalog.name} from ${addon.name}:`, error);
+ } finally {
+ setLoadedCatalogCount(prev => prev + 1);
+ }
+ })();
+
+ catalogPromises.push(catalogPromise);
+ catalogIndex++;
+ }
+ }
+ }
+
+ totalCatalogsRef.current = catalogIndex;
+ console.log(`[HomeScreen] Starting to load ${catalogIndex} catalogs progressively...`);
+
+ // Initialize catalogs array with proper length
+ setCatalogs(new Array(catalogIndex).fill(null));
+
+ // Wait for all catalogs to finish loading (success or failure)
+ await Promise.allSettled(catalogPromises);
+ console.log('[HomeScreen] All catalogs processed');
+
+ // Filter out null values to get only successfully loaded catalogs
+ setCatalogs(prevCatalogs => prevCatalogs.filter(catalog => catalog !== null));
+
+ } catch (error) {
+ console.error('[HomeScreen] Error in progressive catalog loading:', error);
+ } finally {
+ setCatalogsLoading(false);
+ }
+ }, []);
+
// Only count feature section as loading if it's enabled in settings
+ // For catalogs, we show them progressively, so only show loading if no catalogs are loaded yet
const isLoading = useMemo(() =>
- (showHeroSection ? featuredLoading : false) || catalogsLoading,
- [showHeroSection, featuredLoading, catalogsLoading]
+ (showHeroSection ? featuredLoading : false) || (catalogsLoading && catalogs.length === 0),
+ [showHeroSection, featuredLoading, catalogsLoading, catalogs.length]
);
// React to settings changes
@@ -417,6 +524,21 @@ const HomeScreen = () => {
setFeaturedContentSource(settings.featuredContentSource);
}, [settings]);
+ // Load catalogs progressively on mount and when settings change
+ useEffect(() => {
+ loadCatalogsProgressively();
+ }, [loadCatalogsProgressively]);
+
+ // Listen for catalog changes (addon additions/removals) and reload catalogs
+ useEffect(() => {
+ loadCatalogsProgressively();
+ }, [lastUpdate, loadCatalogsProgressively]);
+
+ // Create a refresh function for catalogs
+ const refreshCatalogs = useCallback(() => {
+ return loadCatalogsProgressively();
+ }, [loadCatalogsProgressively]);
+
// Subscribe directly to settings emitter for immediate updates
useEffect(() => {
const handleSettingsChange = () => {
@@ -567,10 +689,13 @@ const HomeScreen = () => {
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
refreshContinueWatching();
+ // Also refresh catalogs when returning to home screen
+ // This ensures new addons are shown even if the context event was missed
+ loadCatalogsProgressively();
});
return unsubscribe;
- }, [navigation, refreshContinueWatching]);
+ }, [navigation, refreshContinueWatching, loadCatalogsProgressively]);
// Memoize the loading screen to prevent unnecessary re-renders
const renderLoadingScreen = useMemo(() => {
@@ -626,28 +751,63 @@ const HomeScreen = () => {
- {catalogs.length > 0 ? (
- catalogs.map((catalog, index) => (
-
+ {/* Show catalogs as they load */}
+ {catalogs.map((catalog, index) => {
+ if (!catalog) {
+ // Show placeholder for loading catalog
+ return (
+
+
+
+
+
+
+ {[...Array(4)].map((_, posterIndex) => (
+
+ ))}
+
+
+ );
+ }
+
+ return (
+
-
- ))
- ) : (
- !catalogsLoading && (
-
-
-
- No content available
-
- navigation.navigate('Settings')}
- >
-
- Add Catalogs
-
-
- )
+
+ );
+ })}
+
+ {/* Show loading indicator for remaining catalogs */}
+ {catalogsLoading && catalogs.length < totalCatalogsRef.current && (
+
+
+
+ Loading more content... ({loadedCatalogCount}/{totalCatalogsRef.current})
+
+
+ )}
+
+ {/* Show empty state only if all catalogs are loaded and none are available */}
+ {!catalogsLoading && catalogs.length === 0 && (
+
+
+
+ No content available
+
+ navigation.navigate('Settings')}
+ >
+
+ Add Catalogs
+
+
)}
@@ -671,26 +831,40 @@ const HomeScreen = () => {
const { width, height } = Dimensions.get('window');
-// Dynamic poster calculation based on screen width
+// Dynamic poster calculation based on screen width - show 1/4 of next poster
const calculatePosterLayout = (screenWidth: number) => {
- const MIN_POSTER_WIDTH = 110; // Minimum poster width for readability
- const MAX_POSTER_WIDTH = 140; // Maximum poster width to prevent oversized posters
- const HORIZONTAL_PADDING = 50; // Total horizontal padding/margins
+ const MIN_POSTER_WIDTH = 100; // Reduced minimum for more posters
+ const MAX_POSTER_WIDTH = 130; // Reduced maximum for more posters
+ const LEFT_PADDING = 16; // Left padding
+ const SPACING = 8; // Space between posters
- // Calculate how many posters can fit
- const availableWidth = screenWidth - HORIZONTAL_PADDING;
- const maxColumns = Math.floor(availableWidth / MIN_POSTER_WIDTH);
+ // Calculate available width for posters (reserve space for left padding)
+ const availableWidth = screenWidth - LEFT_PADDING;
- // Limit to reasonable number of columns (3-6)
- const numColumns = Math.min(Math.max(maxColumns, 3), 6);
+ // Try different numbers of full posters to find the best fit
+ let bestLayout = { numFullPosters: 3, posterWidth: 120 };
- // Calculate actual poster width
- const posterWidth = Math.min(availableWidth / numColumns, MAX_POSTER_WIDTH);
+ for (let n = 3; n <= 6; n++) {
+ // Calculate poster width needed for N full posters + 0.25 partial poster
+ // Formula: N * posterWidth + (N-1) * spacing + 0.25 * posterWidth = availableWidth - rightPadding
+ // Simplified: posterWidth * (N + 0.25) + (N-1) * spacing = availableWidth - rightPadding
+ // We'll use minimal right padding (8px) to maximize space
+ const usableWidth = availableWidth - 8;
+ const posterWidth = (usableWidth - (n - 1) * SPACING) / (n + 0.25);
+
+ console.log(`[HomeScreen] Testing ${n} posters: width=${posterWidth.toFixed(1)}px, screen=${screenWidth}px`);
+
+ if (posterWidth >= MIN_POSTER_WIDTH && posterWidth <= MAX_POSTER_WIDTH) {
+ bestLayout = { numFullPosters: n, posterWidth };
+ console.log(`[HomeScreen] Selected layout: ${n} full posters at ${posterWidth.toFixed(1)}px each`);
+ }
+ }
return {
- numColumns,
- posterWidth,
- spacing: 12 // Space between posters
+ numFullPosters: bestLayout.numFullPosters,
+ posterWidth: bestLayout.posterWidth,
+ spacing: SPACING,
+ partialPosterWidth: bestLayout.posterWidth * 0.25 // 1/4 of next poster
};
};
@@ -714,6 +888,42 @@ const styles = StyleSheet.create({
marginTop: 12,
fontSize: 14,
},
+ loadingMoreCatalogs: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 16,
+ marginHorizontal: 16,
+ marginBottom: 16,
+ },
+ loadingMoreText: {
+ marginLeft: 12,
+ fontSize: 14,
+ },
+ catalogPlaceholder: {
+ marginBottom: 24,
+ paddingHorizontal: 16,
+ },
+ placeholderHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 12,
+ },
+ placeholderTitle: {
+ width: 150,
+ height: 20,
+ borderRadius: 4,
+ },
+ placeholderPosters: {
+ flexDirection: 'row',
+ gap: 8,
+ },
+ placeholderPoster: {
+ width: POSTER_WIDTH,
+ aspectRatio: 2/3,
+ borderRadius: 4,
+ },
emptyCatalog: {
padding: 32,
alignItems: 'center',
@@ -903,7 +1113,8 @@ const styles = StyleSheet.create({
marginRight: 4,
},
catalogList: {
- paddingHorizontal: 16,
+ paddingLeft: 16,
+ paddingRight: 16 - posterLayout.partialPosterWidth,
paddingBottom: 12,
paddingTop: 6,
},
@@ -911,21 +1122,21 @@ const styles = StyleSheet.create({
width: POSTER_WIDTH,
aspectRatio: 2/3,
margin: 0,
- borderRadius: 8,
+ borderRadius: 4,
overflow: 'hidden',
position: 'relative',
- elevation: 8,
+ elevation: 6,
shadowColor: '#000',
- shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.3,
- shadowRadius: 8,
- borderWidth: 1,
- borderColor: 'rgba(255,255,255,0.08)',
+ shadowOffset: { width: 0, height: 3 },
+ shadowOpacity: 0.25,
+ shadowRadius: 6,
+ borderWidth: 0.5,
+ borderColor: 'rgba(255,255,255,0.12)',
},
poster: {
width: '100%',
height: '100%',
- borderRadius: 8,
+ borderRadius: 4,
},
imdbLogo: {
width: 35,
@@ -964,7 +1175,7 @@ const styles = StyleSheet.create({
contentItemContainer: {
width: '100%',
height: '100%',
- borderRadius: 8,
+ borderRadius: 4,
overflow: 'hidden',
position: 'relative',
},