From cf5cc2d8f9ab31878d4b2faf2b469604cef26931 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sun, 28 Dec 2025 22:22:18 +0530 Subject: [PATCH] discover screen init --- src/components/common/Poster.tsx | 268 ++++++++++++++++++ .../home/ContinueWatchingSection.tsx | 41 +-- .../metadata/MoreLikeThisSection.tsx | 39 ++- src/screens/DownloadsScreen.tsx | 19 +- src/screens/LibraryScreen.tsx | 17 +- src/screens/SearchScreen.tsx | 19 +- 6 files changed, 353 insertions(+), 50 deletions(-) create mode 100644 src/components/common/Poster.tsx diff --git a/src/components/common/Poster.tsx b/src/components/common/Poster.tsx new file mode 100644 index 0000000..a6e8e24 --- /dev/null +++ b/src/components/common/Poster.tsx @@ -0,0 +1,268 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { + View, + Text, + StyleSheet, + Platform, + Dimensions, + ViewStyle, +} from 'react-native'; +import FastImage from '@d11/react-native-fast-image'; +import { MaterialIcons } from '@expo/vector-icons'; +import { useTheme } from '../../contexts/ThemeContext'; +import { useSettings } from '../../hooks/useSettings'; + +const { width } = Dimensions.get('window'); + +// Enhanced responsive breakpoints +const BREAKPOINTS = { + phone: 0, + tablet: 768, + largeTablet: 1024, + tv: 1440, +}; + +const getDeviceType = (screenWidth: number) => { + if (screenWidth >= BREAKPOINTS.tv) return 'tv'; + if (screenWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; + if (screenWidth >= BREAKPOINTS.tablet) return 'tablet'; + return 'phone'; +}; + +export type PosterShape = 'poster' | 'landscape' | 'square'; + +export interface PosterProps { + /** The poster image URL */ + uri?: string | null; + /** Width of the poster */ + width: number; + /** Shape of the poster - determines aspect ratio */ + shape?: PosterShape; + /** Optional custom aspect ratio override */ + aspectRatio?: number; + /** Optional custom border radius (uses settings.posterBorderRadius by default) */ + borderRadius?: number; + /** Optional title to display below the poster */ + title?: string; + /** Whether to show the title */ + showTitle?: boolean; + /** Fallback text to show when no poster is available */ + fallbackText?: string; + /** Additional styles for the container */ + style?: ViewStyle; + /** Additional styles for the poster container */ + posterStyle?: ViewStyle; +} + +/** + * Shared Poster component with consistent styling across the app. + * Matches the design from ContentItem.tsx with: + * - Border: 1.5px solid rgba(255,255,255,0.15) + * - Border Radius: settings.posterBorderRadius (default 12) + * - Shadow: elevation 1 on Android, subtle shadow on iOS + * - Aspect Ratio: 2/3 for poster, 16/9 for landscape, 1/1 for square + */ +export const Poster: React.FC = ({ + uri, + width: posterWidth, + shape = 'poster', + aspectRatio: customAspectRatio, + borderRadius: customBorderRadius, + title, + showTitle = false, + fallbackText, + style, + posterStyle, +}) => { + const { currentTheme } = useTheme(); + const { settings, isLoaded } = useSettings(); + const [imageError, setImageError] = useState(false); + + // Reset error state when URI changes + useEffect(() => { + setImageError(false); + }, [uri]); + + // Determine aspect ratio based on shape + const aspectRatio = useMemo(() => { + if (customAspectRatio) return customAspectRatio; + switch (shape) { + case 'landscape': + return 16 / 9; + case 'square': + return 1; + case 'poster': + default: + return 2 / 3; + } + }, [shape, customAspectRatio]); + + // Border radius from settings or custom + const borderRadius = customBorderRadius ?? + (typeof settings.posterBorderRadius === 'number' ? settings.posterBorderRadius : 12); + + // Device type for responsive title sizing + const deviceType = getDeviceType(width); + + // Title font size based on device type + const titleFontSize = useMemo(() => { + switch (deviceType) { + case 'tv': + return 16; + case 'largeTablet': + return 15; + case 'tablet': + return 14; + default: + return 13; + } + }, [deviceType]); + + // Optimize poster URL for TMDB + const optimizedUrl = useMemo(() => { + if (!uri || uri.includes('placeholder')) { + return null; + } + if (uri.includes('image.tmdb.org')) { + return uri.replace(/\/w\d+\//, '/w154/'); + } + return uri; + }, [uri]); + + // Placeholder while settings load + if (!isLoaded) { + return ( + + + {showTitle && } + + ); + } + + return ( + + + {optimizedUrl && !imageError ? ( + setImageError(false)} + onError={() => setImageError(true)} + /> + ) : ( + + {imageError ? ( + + ) : fallbackText ? ( + + {fallbackText.length > 20 ? `${fallbackText.substring(0, 20)}...` : fallbackText} + + ) : ( + + )} + + )} + + + {showTitle && title && ( + + {title} + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: {}, + posterContainer: { + overflow: 'hidden', + position: 'relative', + // Consistent shadow/elevation matching ContentItem + elevation: Platform.OS === 'android' ? 1 : 0, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 1, + // Consistent border styling + borderWidth: 1.5, + borderColor: 'rgba(255,255,255,0.15)', + marginBottom: 8, + }, + poster: { + width: '100%', + height: '100%', + }, + fallbackContainer: { + justifyContent: 'center', + alignItems: 'center', + }, + fallbackText: { + fontSize: 10, + textAlign: 'center', + paddingHorizontal: 4, + }, + title: { + fontWeight: '500', + marginTop: 4, + textAlign: 'center', + }, +}); + +export default Poster; diff --git a/src/components/home/ContinueWatchingSection.tsx b/src/components/home/ContinueWatchingSection.tsx index 96d132c..95236cd 100644 --- a/src/components/home/ContinueWatchingSection.tsx +++ b/src/components/home/ContinueWatchingSection.tsx @@ -1089,7 +1089,8 @@ const ContinueWatchingSection = React.forwardRef((props, re borderColor: currentTheme.colors.border, shadowColor: currentTheme.colors.black, width: computedItemWidth, - height: computedItemHeight + height: computedItemHeight, + borderRadius: settings.posterBorderRadius ?? 12, } ]} activeOpacity={0.8} @@ -1110,7 +1111,7 @@ const ContinueWatchingSection = React.forwardRef((props, re priority: FastImage.priority.high, cache: FastImage.cacheControl.immutable }} - style={styles.continueWatchingPoster} + style={[styles.continueWatchingPoster, { borderTopLeftRadius: settings.posterBorderRadius ?? 12, borderBottomLeftRadius: settings.posterBorderRadius ?? 12 }]} resizeMode={FastImage.resizeMode.cover} /> @@ -1348,13 +1349,15 @@ const styles = StyleSheet.create({ width: 280, height: 120, flexDirection: 'row', - borderRadius: 14, + borderRadius: 12, overflow: 'hidden', - elevation: 6, - shadowOffset: { width: 0, height: 3 }, - shadowOpacity: 0.2, - shadowRadius: 6, - borderWidth: 1, + elevation: 1, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 1, + borderWidth: 1.5, + borderColor: 'rgba(255,255,255,0.15)', }, posterContainer: { width: 80, @@ -1364,8 +1367,8 @@ const styles = StyleSheet.create({ continueWatchingPoster: { width: '100%', height: '100%', - borderTopLeftRadius: 14, - borderBottomLeftRadius: 14, + borderTopLeftRadius: 12, + borderBottomLeftRadius: 12, }, deletingOverlay: { position: 'absolute', @@ -1451,26 +1454,28 @@ const styles = StyleSheet.create({ width: POSTER_WIDTH, aspectRatio: 2 / 3, margin: 0, - borderRadius: 8, + borderRadius: 12, overflow: 'hidden', position: 'relative', - elevation: 8, - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - borderWidth: 1, + elevation: 1, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 1, + borderWidth: 1.5, + borderColor: 'rgba(255,255,255,0.15)', }, contentItemContainer: { width: '100%', height: '100%', - borderRadius: 8, + borderRadius: 12, overflow: 'hidden', position: 'relative', }, poster: { width: '100%', height: '100%', - borderRadius: 8, + borderRadius: 12, }, episodeInfoContainer: { position: 'absolute', diff --git a/src/components/metadata/MoreLikeThisSection.tsx b/src/components/metadata/MoreLikeThisSection.tsx index b09044d..1d5364f 100644 --- a/src/components/metadata/MoreLikeThisSection.tsx +++ b/src/components/metadata/MoreLikeThisSection.tsx @@ -7,6 +7,7 @@ import { TouchableOpacity, ActivityIndicator, Dimensions, + Platform, } from 'react-native'; import FastImage from '@d11/react-native-fast-image'; import { useNavigation, StackActions } from '@react-navigation/native'; @@ -14,6 +15,7 @@ import { NavigationProp } from '@react-navigation/native'; import { RootStackParamList } from '../../navigation/AppNavigator'; import { StreamingContent } from '../../services/catalogService'; import { useTheme } from '../../contexts/ThemeContext'; +import { useSettings } from '../../hooks/useSettings'; import { TMDBService } from '../../services/tmdbService'; import { catalogService } from '../../services/catalogService'; import CustomAlert from '../../components/CustomAlert'; @@ -33,12 +35,14 @@ interface MoreLikeThisSectionProps { loadingRecommendations: boolean; } -export const MoreLikeThisSection: React.FC = ({ - recommendations, - loadingRecommendations +export const MoreLikeThisSection: React.FC = ({ + recommendations, + loadingRecommendations }) => { const { currentTheme } = useTheme(); + const { settings } = useSettings(); const navigation = useNavigation>(); + const borderRadius = settings.posterBorderRadius ?? 12; // Determine device type const deviceWidth = Dimensions.get('window').width; @@ -91,16 +95,16 @@ export const MoreLikeThisSection: React.FC = ({ try { // Extract TMDB ID from the tmdb:123456 format const tmdbId = item.id.replace('tmdb:', ''); - + // Get Stremio ID directly using catalogService // The catalogService.getStremioId method already handles the conversion internally const stremioId = await catalogService.getStremioId(item.type, tmdbId); - + if (stremioId) { navigation.dispatch( - StackActions.push('Metadata', { - id: stremioId, - type: item.type + StackActions.push('Metadata', { + id: stremioId, + type: item.type }) ); } else { @@ -110,19 +114,19 @@ export const MoreLikeThisSection: React.FC = ({ if (__DEV__) console.error('Error navigating to recommendation:', error); setAlertTitle('Error'); setAlertMessage('Unable to load this content. Please try again later.'); - setAlertActions([{ label: 'OK', onPress: () => {} }]); + setAlertActions([{ label: 'OK', onPress: () => { } }]); setAlertVisible(true); } }; const renderItem = ({ item }: { item: StreamingContent }) => ( - handleItemPress(item)} > @@ -144,7 +148,7 @@ export const MoreLikeThisSection: React.FC = ({ } return ( - + More Like This void; }> = React.memo(({ item, onPress, onAction, onRequestRemove }) => { const { currentTheme } = useTheme(); + const { settings } = useSettings(); const { showSuccess, showInfo } = useToast(); const [posterUrl, setPosterUrl] = useState(item.posterUrl || null); + const borderRadius = settings.posterBorderRadius ?? 12; // Try to fetch poster if not available useEffect(() => { @@ -212,10 +214,10 @@ const DownloadItemComponent: React.FC<{ activeOpacity={0.8} > {/* Poster */} - + {/* Status indicator overlay */} @@ -790,16 +792,25 @@ const styles = StyleSheet.create({ posterContainer: { width: POSTER_WIDTH, height: POSTER_HEIGHT, - borderRadius: 8, + borderRadius: 12, marginRight: isTablet ? 20 : 16, position: 'relative', overflow: 'hidden', backgroundColor: '#333', + // Consistent border styling matching ContentItem + borderWidth: 1.5, + borderColor: 'rgba(255,255,255,0.15)', + // Consistent shadow/elevation + elevation: Platform.OS === 'android' ? 1 : 0, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 1, }, poster: { width: '100%', height: '100%', - borderRadius: 8, + borderRadius: 12, }, statusOverlay: { position: 'absolute', diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index ef90ee7..39c0cfa 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -405,10 +405,10 @@ const LibraryScreen = () => { activeOpacity={0.7} > - + {item.watched && ( @@ -1063,11 +1063,14 @@ const styles = StyleSheet.create({ overflow: 'hidden', backgroundColor: 'rgba(255,255,255,0.03)', aspectRatio: 2 / 3, - elevation: 5, - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.2, - shadowRadius: 8, - borderWidth: 1, + // Consistent shadow/elevation matching ContentItem + elevation: Platform.OS === 'android' ? 1 : 0, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 1, + // Consistent border styling + borderWidth: 1.5, borderColor: 'rgba(255,255,255,0.15)', }, poster: { diff --git a/src/screens/SearchScreen.tsx b/src/screens/SearchScreen.tsx index 81dd419..9bdfff4 100644 --- a/src/screens/SearchScreen.tsx +++ b/src/screens/SearchScreen.tsx @@ -1014,16 +1014,14 @@ const SearchScreen = () => { > {/* Bookmark and watched icons top right, bookmark to the left of watched */} @@ -1723,10 +1721,17 @@ const styles = StyleSheet.create({ horizontalItemPosterContainer: { width: HORIZONTAL_ITEM_WIDTH, height: HORIZONTAL_POSTER_HEIGHT, - borderRadius: 16, + borderRadius: 12, overflow: 'hidden', marginBottom: 8, - borderWidth: 1, + borderWidth: 1.5, + borderColor: 'rgba(255,255,255,0.15)', + // Consistent shadow/elevation matching ContentItem + elevation: Platform.OS === 'android' ? 1 : 0, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 1, }, horizontalItemPoster: { width: '100%',