mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
ui fixes
This commit is contained in:
parent
5545ed40fb
commit
9cb2049d6b
3 changed files with 334 additions and 95 deletions
|
|
@ -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={() => <View style={{ width: 12 }} />}
|
||||
ItemSeparatorComponent={() => <View style={{ width: 8 }} />}
|
||||
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,
|
||||
})}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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<ContinueWatchingRef>(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<NodeJS.Timeout | null>(null);
|
||||
const [hasContinueWatching, setHasContinueWatching] = useState(false);
|
||||
|
||||
const {
|
||||
catalogs,
|
||||
loading: catalogsLoading,
|
||||
refreshCatalogs
|
||||
} = useHomeCatalogs();
|
||||
const [catalogs, setCatalogs] = useState<CatalogContent[]>([]);
|
||||
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<void>[] = [];
|
||||
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 = () => {
|
|||
|
||||
<ContinueWatchingSection ref={continueWatchingRef} />
|
||||
|
||||
{catalogs.length > 0 ? (
|
||||
catalogs.map((catalog, index) => (
|
||||
<View key={`${catalog.addon}-${catalog.id}-${index}`}>
|
||||
{/* Show catalogs as they load */}
|
||||
{catalogs.map((catalog, index) => {
|
||||
if (!catalog) {
|
||||
// Show placeholder for loading catalog
|
||||
return (
|
||||
<View key={`placeholder-${index}`} style={styles.catalogPlaceholder}>
|
||||
<View style={styles.placeholderHeader}>
|
||||
<View style={[styles.placeholderTitle, { backgroundColor: currentTheme.colors.elevation1 }]} />
|
||||
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
|
||||
</View>
|
||||
<View style={styles.placeholderPosters}>
|
||||
{[...Array(4)].map((_, posterIndex) => (
|
||||
<View
|
||||
key={posterIndex}
|
||||
style={[styles.placeholderPoster, { backgroundColor: currentTheme.colors.elevation1 }]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
key={`${catalog.addon}-${catalog.id}-${index}`}
|
||||
entering={FadeIn.duration(300)}
|
||||
>
|
||||
<CatalogSection catalog={catalog} />
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
!catalogsLoading && (
|
||||
<View style={[styles.emptyCatalog, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
<MaterialIcons name="movie-filter" size={40} color={currentTheme.colors.textDark} />
|
||||
<Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
|
||||
No content available
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={() => navigation.navigate('Settings')}
|
||||
>
|
||||
<MaterialIcons name="add-circle" size={20} color={currentTheme.colors.white} />
|
||||
<Text style={[styles.addCatalogButtonText, { color: currentTheme.colors.white }]}>Add Catalogs</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
</Animated.View>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Show loading indicator for remaining catalogs */}
|
||||
{catalogsLoading && catalogs.length < totalCatalogsRef.current && (
|
||||
<View style={styles.loadingMoreCatalogs}>
|
||||
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
|
||||
<Text style={[styles.loadingMoreText, { color: currentTheme.colors.textMuted }]}>
|
||||
Loading more content... ({loadedCatalogCount}/{totalCatalogsRef.current})
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Show empty state only if all catalogs are loaded and none are available */}
|
||||
{!catalogsLoading && catalogs.length === 0 && (
|
||||
<View style={[styles.emptyCatalog, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
<MaterialIcons name="movie-filter" size={40} color={currentTheme.colors.textDark} />
|
||||
<Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
|
||||
No content available
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={() => navigation.navigate('Settings')}
|
||||
>
|
||||
<MaterialIcons name="add-circle" size={20} color={currentTheme.colors.white} />
|
||||
<Text style={[styles.addCatalogButtonText, { color: currentTheme.colors.white }]}>Add Catalogs</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
|
@ -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<any>({
|
|||
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<any>({
|
|||
marginRight: 4,
|
||||
},
|
||||
catalogList: {
|
||||
paddingHorizontal: 16,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16 - posterLayout.partialPosterWidth,
|
||||
paddingBottom: 12,
|
||||
paddingTop: 6,
|
||||
},
|
||||
|
|
@ -911,21 +1122,21 @@ const styles = StyleSheet.create<any>({
|
|||
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<any>({
|
|||
contentItemContainer: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 8,
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue