From 188c6e37f1f4dc6d5faf7f77253fa7d9111d3f67 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sun, 4 May 2025 01:17:08 +0530 Subject: [PATCH] Integrate theme context across metadata components for enhanced UI consistency This update refactors multiple metadata components, including CastSection, FloatingHeader, HeroSection, and RatingsSection, to utilize the new ThemeContext for dynamic theming. Styles have been adjusted to reflect the current theme colors, improving visual consistency throughout the application. Additionally, loading indicators and text colors have been updated to align with the theme, enhancing the overall user experience. These changes streamline the components and ensure a cohesive interface across different themes. --- src/components/metadata/CastSection.tsx | 181 ++++++++++-------- src/components/metadata/FloatingHeader.tsx | 28 ++- src/components/metadata/HeroSection.tsx | 56 +++--- src/components/metadata/MetadataDetails.tsx | 31 ++- .../metadata/MoreLikeThisSection.tsx | 14 +- src/components/metadata/MovieContent.tsx | 17 +- src/components/metadata/RatingsSection.tsx | 175 ++++++++++------- src/screens/MetadataScreen.tsx | 42 ++-- 8 files changed, 310 insertions(+), 234 deletions(-) diff --git a/src/components/metadata/CastSection.tsx b/src/components/metadata/CastSection.tsx index 8c9e3e6..f904eb7 100644 --- a/src/components/metadata/CastSection.tsx +++ b/src/components/metadata/CastSection.tsx @@ -3,20 +3,21 @@ import { View, Text, StyleSheet, + FlatList, TouchableOpacity, ActivityIndicator, - ScrollView, } from 'react-native'; import { Image } from 'expo-image'; -import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; -import { colors } from '../../styles/colors'; -import { Cast } from '../../types/metadata'; -import { tmdbService } from '../../services/tmdbService'; +import Animated, { + FadeIn, + Layout, +} from 'react-native-reanimated'; +import { useTheme } from '../../contexts/ThemeContext'; interface CastSectionProps { - cast: Cast[]; + cast: any[]; loadingCast: boolean; - onSelectCastMember: (member: Cast) => void; + onSelectCastMember: (castMember: any) => void; } export const CastSection: React.FC = ({ @@ -24,123 +25,137 @@ export const CastSection: React.FC = ({ loadingCast, onSelectCastMember, }) => { + const { currentTheme } = useTheme(); + if (loadingCast) { return ( - + ); } - if (!cast.length) { + if (!cast || cast.length === 0) { return null; } return ( - - Cast - + + Cast + + - {cast.map((member) => ( - onSelectCastMember(member)} + contentContainerStyle={styles.castList} + keyExtractor={(item) => item.id.toString()} + renderItem={({ item, index }) => ( + - - {member.profile_path ? ( - - ) : ( - + onSelectCastMember(item)} + activeOpacity={0.7} + > + + {item.profile_path ? ( + + ) : ( + + + {item.name.split(' ').reduce((prev: string, current: string) => prev + current[0], '').substring(0, 2)} + + + )} + + {item.name} + {item.character && ( + {item.character} )} - - - {member.name} - {member.character} - - - ))} - - + + + )} + /> + ); }; const styles = StyleSheet.create({ + castSection: { + marginBottom: 24, + paddingHorizontal: 0, + }, loadingContainer: { + paddingVertical: 20, alignItems: 'center', justifyContent: 'center', - padding: 12, }, - castSection: { - marginTop: 0, - paddingLeft: 0, - }, - sectionTitle: { - color: colors.highEmphasis, - fontSize: 18, - fontWeight: '700', - marginBottom: 10, + sectionHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 12, paddingHorizontal: 16, }, - castScrollContainer: { - marginTop: 4, + sectionTitle: { + fontSize: 18, + fontWeight: '700', }, - castContainer: { - paddingHorizontal: 12, - paddingVertical: 4, + castList: { + paddingHorizontal: 16, + paddingBottom: 4, }, - castMember: { - width: 80, - marginRight: 12, + castCard: { + marginRight: 16, + width: 90, alignItems: 'center', }, castImageContainer: { - width: 64, - height: 64, - borderRadius: 32, - backgroundColor: colors.elevation2, - justifyContent: 'center', - alignItems: 'center', + width: 80, + height: 80, + borderRadius: 40, overflow: 'hidden', - marginBottom: 6, - borderWidth: 1, - borderColor: 'rgba(255,255,255,0.1)', + marginBottom: 8, }, castImage: { - width: 64, - height: 64, - borderRadius: 32, - }, - castTextContainer: { width: '100%', + height: '100%', + }, + castImagePlaceholder: { + width: '100%', + height: '100%', + borderRadius: 40, alignItems: 'center', + justifyContent: 'center', + }, + placeholderText: { + fontSize: 24, + fontWeight: '600', }, castName: { - color: colors.highEmphasis, - fontSize: 13, + fontSize: 14, fontWeight: '600', textAlign: 'center', + width: 90, }, - castCharacter: { - color: colors.mediumEmphasis, + characterName: { fontSize: 12, textAlign: 'center', + width: 90, marginTop: 2, - opacity: 0.8, }, }); \ No newline at end of file diff --git a/src/components/metadata/FloatingHeader.tsx b/src/components/metadata/FloatingHeader.tsx index 619723f..30bdfbb 100644 --- a/src/components/metadata/FloatingHeader.tsx +++ b/src/components/metadata/FloatingHeader.tsx @@ -16,7 +16,7 @@ import Animated, { interpolate, Extrapolate, } from 'react-native-reanimated'; -import { colors } from '../../styles/colors'; +import { useTheme } from '../../contexts/ThemeContext'; import { logger } from '../../utils/logger'; const { width } = Dimensions.get('window'); @@ -46,6 +46,8 @@ const FloatingHeader: React.FC = ({ safeAreaTop, setLogoLoadError, }) => { + const { currentTheme } = useTheme(); + // Animated styles for the header const headerAnimatedStyle = useAnimatedStyle(() => ({ opacity: headerOpacity.value, @@ -74,7 +76,11 @@ const FloatingHeader: React.FC = ({ onPress={handleBack} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > - + @@ -90,7 +96,7 @@ const FloatingHeader: React.FC = ({ }} /> ) : ( - {metadata.name} + {metadata.name} )} @@ -102,7 +108,7 @@ const FloatingHeader: React.FC = ({ @@ -121,7 +127,11 @@ const FloatingHeader: React.FC = ({ onPress={handleBack} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > - + @@ -137,7 +147,7 @@ const FloatingHeader: React.FC = ({ }} /> ) : ( - {metadata.name} + {metadata.name} )} @@ -149,13 +159,13 @@ const FloatingHeader: React.FC = ({ )} - {Platform.OS === 'ios' && } + {Platform.OS === 'ios' && } ); }; @@ -190,7 +200,6 @@ const styles = StyleSheet.create({ left: 0, right: 0, height: 0.5, - backgroundColor: 'rgba(255,255,255,0.15)', }, headerTitleContainer: { flex: 1, @@ -218,7 +227,6 @@ const styles = StyleSheet.create({ maxWidth: 240, }, floatingHeaderTitle: { - color: colors.highEmphasis, fontSize: 18, fontWeight: '700', textAlign: 'center', diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index bd1c1cd..cef9ec4 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -14,7 +14,7 @@ import Animated, { interpolate, Extrapolate, } from 'react-native-reanimated'; -import { colors } from '../../styles/colors'; +import { useTheme } from '../../contexts/ThemeContext'; import { logger } from '../../utils/logger'; import { TMDBService } from '../../services/tmdbService'; @@ -77,6 +77,7 @@ const ActionButtons = React.memo(({ playButtonText: string; animatedStyle: any; }) => { + const { currentTheme } = useTheme(); return ( {inLibrary ? 'Saved' : 'Save'} @@ -155,7 +156,11 @@ const ActionButtons = React.memo(({ } }} > - + )} @@ -174,6 +179,7 @@ const WatchProgressDisplay = React.memo(({ getEpisodeDetails: (episodeId: string) => { seasonNumber: string; episodeNumber: string; episodeName: string } | null; animatedStyle: any; }) => { + const { currentTheme } = useTheme(); if (!watchProgress || watchProgress.duration === 0) { return null; } @@ -195,11 +201,14 @@ const WatchProgressDisplay = React.memo(({ - + {progressPercent >= 95 ? 'Watched' : `${Math.round(progressPercent)}% watched`}{episodeInfo} • Last watched on {formattedTime} @@ -236,11 +245,12 @@ const HeroSection: React.FC = ({ setBannerImage, setLogoLoadError, }) => { + const { currentTheme } = useTheme(); // Animated styles const heroAnimatedStyle = useAnimatedStyle(() => ({ width: '100%', height: heroHeight.value, - backgroundColor: colors.black, + backgroundColor: currentTheme.colors.black, transform: [{ scale: heroScale.value }], opacity: heroOpacity.value, })); @@ -309,9 +319,13 @@ const HeroSection: React.FC = ({ return genresToDisplay.slice(0, 4).map((genreName, index, array) => ( - {genreName} + + {genreName} + {index < array.length - 1 && ( - + + • + )} )); @@ -321,7 +335,7 @@ const HeroSection: React.FC = ({ {loadingBanner ? ( - + ) : ( = ({ )} = ({ }} /> ) : ( - {metadata.name} + {metadata.name} )} @@ -405,7 +419,7 @@ const styles = StyleSheet.create({ heroSection: { width: '100%', height: height * 0.5, - backgroundColor: colors.black, + backgroundColor: '#000', overflow: 'hidden', }, absoluteFill: { @@ -442,7 +456,6 @@ const styles = StyleSheet.create({ alignSelf: 'center', }, heroTitle: { - color: colors.highEmphasis, fontSize: 28, fontWeight: '900', marginBottom: 12, @@ -461,15 +474,12 @@ const styles = StyleSheet.create({ gap: 4, }, genreText: { - color: colors.text, fontSize: 12, fontWeight: '500', }, genreDot: { - color: colors.text, fontSize: 12, fontWeight: '500', - opacity: 0.6, marginHorizontal: 4, }, actionButtons: { @@ -494,7 +504,7 @@ const styles = StyleSheet.create({ flex: 1, }, playButton: { - backgroundColor: colors.white, + backgroundColor: '#fff', }, infoButton: { backgroundColor: 'rgba(255,255,255,0.2)', @@ -546,11 +556,9 @@ const styles = StyleSheet.create({ }, watchProgressFill: { height: '100%', - backgroundColor: colors.primary, borderRadius: 1.5, }, watchProgressText: { - color: colors.textMuted, fontSize: 12, textAlign: 'center', opacity: 0.9, diff --git a/src/components/metadata/MetadataDetails.tsx b/src/components/metadata/MetadataDetails.tsx index a32b67b..905dc5a 100644 --- a/src/components/metadata/MetadataDetails.tsx +++ b/src/components/metadata/MetadataDetails.tsx @@ -12,7 +12,7 @@ import Animated, { Easing, FadeIn, } from 'react-native-reanimated'; -import { colors } from '../../styles/colors'; +import { useTheme } from '../../contexts/ThemeContext'; interface MetadataDetailsProps { metadata: any; @@ -25,6 +25,7 @@ const MetadataDetails: React.FC = ({ imdbId, type, }) => { + const { currentTheme } = useTheme(); const [isFullDescriptionOpen, setIsFullDescriptionOpen] = useState(false); return ( @@ -32,13 +33,13 @@ const MetadataDetails: React.FC = ({ {/* Meta Info */} {metadata.year && ( - {metadata.year} + {metadata.year} )} {metadata.runtime && ( - {metadata.runtime} + {metadata.runtime} )} {metadata.certification && ( - {metadata.certification} + {metadata.certification} )} {metadata.imdbRating && ( @@ -47,7 +48,7 @@ const MetadataDetails: React.FC = ({ style={styles.imdbLogo} contentFit="contain" /> - {metadata.imdbRating} + {metadata.imdbRating} )} @@ -59,14 +60,14 @@ const MetadataDetails: React.FC = ({ > {metadata.directors && metadata.directors.length > 0 && ( - Director{metadata.directors.length > 1 ? 's' : ''}: - {metadata.directors.join(', ')} + Director{metadata.directors.length > 1 ? 's' : ''}: + {metadata.directors.join(', ')} )} {metadata.creators && metadata.creators.length > 0 && ( - Creator{metadata.creators.length > 1 ? 's' : ''}: - {metadata.creators.join(', ')} + Creator{metadata.creators.length > 1 ? 's' : ''}: + {metadata.creators.join(', ')} )} @@ -81,17 +82,17 @@ const MetadataDetails: React.FC = ({ onPress={() => setIsFullDescriptionOpen(!isFullDescriptionOpen)} activeOpacity={0.7} > - + {metadata.description} - + {isFullDescriptionOpen ? 'Show Less' : 'Show More'} @@ -110,7 +111,6 @@ const styles = StyleSheet.create({ marginBottom: 12, }, metaText: { - color: colors.text, fontSize: 15, fontWeight: '700', letterSpacing: 0.3, @@ -127,7 +127,6 @@ const styles = StyleSheet.create({ marginRight: 4, }, ratingText: { - color: colors.text, fontWeight: '700', fontSize: 15, letterSpacing: 0.3, @@ -143,14 +142,12 @@ const styles = StyleSheet.create({ height: 20 }, creatorLabel: { - color: colors.white, fontSize: 14, fontWeight: '600', marginRight: 8, lineHeight: 20 }, creatorText: { - color: colors.lightGray, fontSize: 14, flex: 1, lineHeight: 20 @@ -160,7 +157,6 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, }, description: { - color: colors.mediumEmphasis, fontSize: 15, lineHeight: 24, }, @@ -171,7 +167,6 @@ const styles = StyleSheet.create({ paddingVertical: 4, }, showMoreText: { - color: colors.textMuted, fontSize: 14, marginRight: 4, }, diff --git a/src/components/metadata/MoreLikeThisSection.tsx b/src/components/metadata/MoreLikeThisSection.tsx index f9c8563..f69cc69 100644 --- a/src/components/metadata/MoreLikeThisSection.tsx +++ b/src/components/metadata/MoreLikeThisSection.tsx @@ -14,7 +14,7 @@ import { useNavigation, StackActions } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { RootStackParamList } from '../../navigation/AppNavigator'; import { StreamingContent } from '../../types/metadata'; -import { colors } from '../../styles/colors'; +import { useTheme } from '../../contexts/ThemeContext'; import { TMDBService } from '../../services/tmdbService'; import { catalogService } from '../../services/catalogService'; @@ -31,6 +31,7 @@ export const MoreLikeThisSection: React.FC = ({ recommendations, loadingRecommendations }) => { + const { currentTheme } = useTheme(); const navigation = useNavigation>(); const handleItemPress = async (item: StreamingContent) => { @@ -69,11 +70,11 @@ export const MoreLikeThisSection: React.FC = ({ > - + {item.name} @@ -82,7 +83,7 @@ export const MoreLikeThisSection: React.FC = ({ if (loadingRecommendations) { return ( - + ); } @@ -93,7 +94,7 @@ export const MoreLikeThisSection: React.FC = ({ return ( - More Like This + More Like This = ({ metadata }) => { + const { currentTheme } = useTheme(); const hasCast = Array.isArray(metadata.cast) && metadata.cast.length > 0; const castDisplay = hasCast ? (metadata.cast as string[]).slice(0, 5).join(', ') : ''; @@ -17,22 +18,22 @@ export const MovieContent: React.FC = ({ metadata }) => { {metadata.director && ( - Director: - {metadata.director} + Director: + {metadata.director} )} {metadata.writer && ( - Writer: - {metadata.writer} + Writer: + {metadata.writer} )} {hasCast && ( - Cast: - {castDisplay} + Cast: + {castDisplay} )} @@ -53,12 +54,10 @@ const styles = StyleSheet.create({ alignItems: 'flex-start', }, metadataLabel: { - color: colors.textMuted, fontSize: 15, width: 70, }, metadataValue: { - color: colors.text, fontSize: 15, flex: 1, lineHeight: 24, diff --git a/src/components/metadata/RatingsSection.tsx b/src/components/metadata/RatingsSection.tsx index a52076e..f09ea70 100644 --- a/src/components/metadata/RatingsSection.tsx +++ b/src/components/metadata/RatingsSection.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useRef } from 'react'; import { View, Text, StyleSheet, ActivityIndicator, Image, Animated } from 'react-native'; -import { colors } from '../../styles/colors'; +import { useTheme } from '../../contexts/ThemeContext'; import { useMDBListRatings } from '../../hooks/useMDBListRatings'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { isMDBListEnabled, RATING_PROVIDERS_STORAGE_KEY } from '../../screens/MDBListSettingsScreen'; @@ -54,6 +54,7 @@ export const RatingsSection: React.FC = ({ imdbId, type }) const [enabledProviders, setEnabledProviders] = useState>({}); const [isMDBEnabled, setIsMDBEnabled] = useState(true); const fadeAnim = useRef(new Animated.Value(0)).current; + const { currentTheme } = useTheme(); useEffect(() => { loadProviderSettings(); @@ -120,7 +121,7 @@ export const RatingsSection: React.FC = ({ imdbId, type }) if (loading) { return ( - + ); } @@ -214,86 +215,128 @@ export const RatingsSection: React.FC = ({ imdbId, type }) }, ]} > - {displayRatings.map(([source, value]) => { - const config = ratingConfig[source as keyof typeof ratingConfig]; - const numericValue = typeof value === 'string' ? parseFloat(value) : value; - const displayValue = config.transform(numericValue); - - // Get a short display name for the rating source - const getSourceLabel = (src: string): string => { - switch(src) { - case 'imdb': return 'IMDb'; - case 'tmdb': return 'TMDB'; - case 'tomatoes': return 'RT'; - case 'audience': return 'Aud'; - case 'metacritic': return 'Meta'; - case 'letterboxd': return 'LBXD'; - case 'trakt': return 'Trakt'; - default: return src; - } - }; - - return ( - - {config.isImage ? ( - - ) : ( - - )} - - {displayValue}{config.suffix} - - - ); - })} + + Ratings + + + {displayRatings.map(([source, value]) => { + const config = ratingConfig[source as keyof typeof ratingConfig]; + const displayValue = config.transform(parseFloat(value as string)); + + // Get a short display name for the rating source + const getSourceLabel = (src: string): string => { + switch(src) { + case 'imdb': return 'IMDb'; + case 'tmdb': return 'TMDB'; + case 'tomatoes': return 'RT'; + case 'audience': return 'Aud'; + case 'metacritic': return 'Meta'; + case 'letterboxd': return 'LBXD'; + case 'trakt': return 'Trakt'; + default: return src; + } + }; + + return ( + + + {config.isImage ? ( + + ) : ( + + {React.createElement(config.icon as any, { + width: 24, + height: 24, + })} + + )} + + + {config.prefix}{displayValue}{config.suffix} + + {getSourceLabel(source)} + + ); + })} + ); }; const styles = StyleSheet.create({ container: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - marginTop: 8, - marginBottom: 16, - paddingHorizontal: 12, - gap: 4, + marginBottom: 20, + paddingHorizontal: 16, }, loadingContainer: { - alignItems: 'center', + height: 80, justifyContent: 'center', - height: 40, - marginVertical: 16, + alignItems: 'center', }, - ratingItem: { + header: { flexDirection: 'row', alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.4)', - paddingVertical: 3, - paddingHorizontal: 4, - borderRadius: 4, + justifyContent: 'space-between', + marginBottom: 12, }, - ratingIcon: { - width: 16, - height: 16, - marginRight: 3, - alignSelf: 'center', + title: { + fontSize: 18, + fontWeight: '700', + }, + ratingsContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 10, + }, + ratingItem: { + flexDirection: 'column', + alignItems: 'center', + width: 55, + }, + ratingIconContainer: { + width: 32, + height: 32, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 4, + }, + ratingIconImage: { + width: 32, + height: 32, + }, + svgContainer: { + alignItems: 'center', + justifyContent: 'center', }, ratingValue: { - fontSize: 13, - fontWeight: 'bold', + fontSize: 16, + fontWeight: '700', + marginVertical: 2, }, - ratingLabel: { + ratingSource: { fontSize: 11, - opacity: 0.9, + textAlign: 'center', + }, + noRatingsText: { + fontSize: 14, + color: 'gray', + fontStyle: 'italic', + textAlign: 'center', + marginVertical: 16, + }, + errorText: { + fontSize: 12, + color: '#ff0000', + textAlign: 'center', + marginVertical: 8, }, }); \ No newline at end of file diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index 881bdd0..7648477 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -12,7 +12,7 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' import { useRoute, useNavigation } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; import * as Haptics from 'expo-haptics'; -import { colors } from '../styles/colors'; +import { useTheme } from '../contexts/ThemeContext'; import { useMetadata } from '../hooks/useMetadata'; import { CastSection } from '../components/metadata/CastSection'; import { SeriesContent } from '../components/metadata/SeriesContent'; @@ -48,6 +48,9 @@ const MetadataScreen = () => { // Add settings hook const { settings } = useSettings(); + // Get theme context + const { currentTheme } = useTheme(); + // Get safe area insets const { top: safeAreaTop } = useSafeAreaInsets(); @@ -182,7 +185,9 @@ const MetadataScreen = () => { if (loading) { return ( { barStyle="light-content" /> - - + + Loading content... @@ -203,7 +210,9 @@ const MetadataScreen = () => { if (metadataError || !metadata) { return ( { - + {metadataError || 'Content not found'} Try Again @@ -238,11 +249,11 @@ const MetadataScreen = () => { - + Go Back @@ -253,7 +264,9 @@ const MetadataScreen = () => { return (