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.
This commit is contained in:
tapframe 2025-05-04 01:17:08 +05:30
parent 190c1a7371
commit 188c6e37f1
8 changed files with 310 additions and 234 deletions

View file

@ -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<CastSectionProps> = ({
@ -24,123 +25,137 @@ export const CastSection: React.FC<CastSectionProps> = ({
loadingCast,
onSelectCastMember,
}) => {
const { currentTheme } = useTheme();
if (loadingCast) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="small" color={colors.primary} />
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
</View>
);
}
if (!cast.length) {
if (!cast || cast.length === 0) {
return null;
}
return (
<View style={styles.castSection}>
<Text style={styles.sectionTitle}>Cast</Text>
<ScrollView
horizontal
<Animated.View
style={styles.castSection}
entering={FadeIn.duration(500).delay(300)}
layout={Layout}
>
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>Cast</Text>
</View>
<FlatList
horizontal
data={cast}
showsHorizontalScrollIndicator={false}
style={styles.castScrollContainer}
contentContainerStyle={styles.castContainer}
snapToAlignment="start"
>
{cast.map((member) => (
<TouchableOpacity
key={member.id}
style={styles.castMember}
onPress={() => onSelectCastMember(member)}
contentContainerStyle={styles.castList}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item, index }) => (
<Animated.View
entering={FadeIn.duration(500).delay(100 + index * 50)}
layout={Layout}
>
<View style={styles.castImageContainer}>
{member.profile_path ? (
<Image
source={{
uri: `https://image.tmdb.org/t/p/w185${member.profile_path}`
}}
style={styles.castImage}
contentFit="cover"
/>
) : (
<MaterialIcons
name="person"
size={32}
color={colors.textMuted}
/>
<TouchableOpacity
style={styles.castCard}
onPress={() => onSelectCastMember(item)}
activeOpacity={0.7}
>
<View style={styles.castImageContainer}>
{item.profile_path ? (
<Image
source={{
uri: `https://image.tmdb.org/t/p/w185${item.profile_path}`,
}}
style={styles.castImage}
contentFit="cover"
transition={200}
/>
) : (
<View style={[styles.castImagePlaceholder, { backgroundColor: currentTheme.colors.cardBackground }]}>
<Text style={[styles.placeholderText, { color: currentTheme.colors.textMuted }]}>
{item.name.split(' ').reduce((prev: string, current: string) => prev + current[0], '').substring(0, 2)}
</Text>
</View>
)}
</View>
<Text style={[styles.castName, { color: currentTheme.colors.text }]} numberOfLines={1}>{item.name}</Text>
{item.character && (
<Text style={[styles.characterName, { color: currentTheme.colors.textMuted }]} numberOfLines={1}>{item.character}</Text>
)}
</View>
<View style={styles.castTextContainer}>
<Text style={styles.castName} numberOfLines={1}>{member.name}</Text>
<Text style={styles.castCharacter} numberOfLines={1}>{member.character}</Text>
</View>
</TouchableOpacity>
))}
</ScrollView>
</View>
</TouchableOpacity>
</Animated.View>
)}
/>
</Animated.View>
);
};
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,
},
});

View file

@ -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<FloatingHeaderProps> = ({
safeAreaTop,
setLogoLoadError,
}) => {
const { currentTheme } = useTheme();
// Animated styles for the header
const headerAnimatedStyle = useAnimatedStyle(() => ({
opacity: headerOpacity.value,
@ -74,7 +76,11 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
onPress={handleBack}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<MaterialIcons name="arrow-back" size={24} color={colors.highEmphasis} />
<MaterialIcons
name="arrow-back"
size={24}
color={currentTheme.colors.highEmphasis}
/>
</TouchableOpacity>
<View style={styles.headerTitleContainer}>
@ -90,7 +96,7 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
}}
/>
) : (
<Text style={styles.floatingHeaderTitle} numberOfLines={1}>{metadata.name}</Text>
<Text style={[styles.floatingHeaderTitle, { color: currentTheme.colors.highEmphasis }]} numberOfLines={1}>{metadata.name}</Text>
)}
</View>
@ -102,7 +108,7 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
<MaterialIcons
name={inLibrary ? 'bookmark' : 'bookmark-border'}
size={22}
color={colors.highEmphasis}
color={currentTheme.colors.highEmphasis}
/>
</TouchableOpacity>
</Animated.View>
@ -121,7 +127,11 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
onPress={handleBack}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<MaterialIcons name="arrow-back" size={24} color={colors.highEmphasis} />
<MaterialIcons
name="arrow-back"
size={24}
color={currentTheme.colors.highEmphasis}
/>
</TouchableOpacity>
<View style={styles.headerTitleContainer}>
@ -137,7 +147,7 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
}}
/>
) : (
<Text style={styles.floatingHeaderTitle} numberOfLines={1}>{metadata.name}</Text>
<Text style={[styles.floatingHeaderTitle, { color: currentTheme.colors.highEmphasis }]} numberOfLines={1}>{metadata.name}</Text>
)}
</View>
@ -149,13 +159,13 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
<MaterialIcons
name={inLibrary ? 'bookmark' : 'bookmark-border'}
size={22}
color={colors.highEmphasis}
color={currentTheme.colors.highEmphasis}
/>
</TouchableOpacity>
</Animated.View>
</View>
)}
{Platform.OS === 'ios' && <View style={styles.headerBottomBorder} />}
{Platform.OS === 'ios' && <View style={[styles.headerBottomBorder, { backgroundColor: 'rgba(255,255,255,0.15)' }]} />}
</Animated.View>
);
};
@ -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',

View file

@ -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 (
<Animated.View style={[styles.actionButtons, animatedStyle]}>
<TouchableOpacity
@ -100,7 +101,7 @@ const ActionButtons = React.memo(({
<MaterialIcons
name={inLibrary ? 'bookmark' : 'bookmark-border'}
size={24}
color="#fff"
color={currentTheme.colors.white}
/>
<Text style={styles.infoButtonText}>
{inLibrary ? 'Saved' : 'Save'}
@ -155,7 +156,11 @@ const ActionButtons = React.memo(({
}
}}
>
<MaterialIcons name="assessment" size={24} color="#fff" />
<MaterialIcons
name="assessment"
size={24}
color={currentTheme.colors.white}
/>
</TouchableOpacity>
)}
</Animated.View>
@ -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(({
<View
style={[
styles.watchProgressFill,
{ width: `${progressPercent}%` }
{
width: `${progressPercent}%`,
backgroundColor: currentTheme.colors.primary
}
]}
/>
</View>
<Text style={styles.watchProgressText}>
<Text style={[styles.watchProgressText, { color: currentTheme.colors.textMuted }]}>
{progressPercent >= 95 ? 'Watched' : `${Math.round(progressPercent)}% watched`}{episodeInfo} Last watched on {formattedTime}
</Text>
</Animated.View>
@ -236,11 +245,12 @@ const HeroSection: React.FC<HeroSectionProps> = ({
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<HeroSectionProps> = ({
return genresToDisplay.slice(0, 4).map((genreName, index, array) => (
<React.Fragment key={index}>
<Text style={styles.genreText}>{genreName}</Text>
<Text style={[styles.genreText, { color: currentTheme.colors.text }]}>
{genreName}
</Text>
{index < array.length - 1 && (
<Text style={styles.genreDot}></Text>
<Text style={[styles.genreDot, { color: currentTheme.colors.text, opacity: 0.6 }]}>
</Text>
)}
</React.Fragment>
));
@ -321,7 +335,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({
<Animated.View style={heroAnimatedStyle}>
<View style={styles.heroSection}>
{loadingBanner ? (
<View style={[styles.absoluteFill, { backgroundColor: colors.black }]} />
<View style={[styles.absoluteFill, { backgroundColor: currentTheme.colors.black }]} />
) : (
<Animated.Image
source={{ uri: bannerImage || metadata.banner || metadata.poster }}
@ -337,12 +351,12 @@ const HeroSection: React.FC<HeroSectionProps> = ({
)}
<LinearGradient
colors={[
`${colors.darkBackground}00`,
`${colors.darkBackground}20`,
`${colors.darkBackground}50`,
`${colors.darkBackground}C0`,
`${colors.darkBackground}F8`,
colors.darkBackground
`${currentTheme.colors.darkBackground}00`,
`${currentTheme.colors.darkBackground}20`,
`${currentTheme.colors.darkBackground}50`,
`${currentTheme.colors.darkBackground}C0`,
`${currentTheme.colors.darkBackground}F8`,
currentTheme.colors.darkBackground
]}
locations={[0, 0.4, 0.65, 0.8, 0.9, 1]}
style={styles.heroGradient}
@ -363,7 +377,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({
}}
/>
) : (
<Text style={styles.heroTitle}>{metadata.name}</Text>
<Text style={[styles.heroTitle, { color: currentTheme.colors.highEmphasis }]}>{metadata.name}</Text>
)}
</Animated.View>
</View>
@ -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,

View file

@ -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<MetadataDetailsProps> = ({
imdbId,
type,
}) => {
const { currentTheme } = useTheme();
const [isFullDescriptionOpen, setIsFullDescriptionOpen] = useState(false);
return (
@ -32,13 +33,13 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
{/* Meta Info */}
<View style={styles.metaInfo}>
{metadata.year && (
<Text style={styles.metaText}>{metadata.year}</Text>
<Text style={[styles.metaText, { color: currentTheme.colors.text }]}>{metadata.year}</Text>
)}
{metadata.runtime && (
<Text style={styles.metaText}>{metadata.runtime}</Text>
<Text style={[styles.metaText, { color: currentTheme.colors.text }]}>{metadata.runtime}</Text>
)}
{metadata.certification && (
<Text style={styles.metaText}>{metadata.certification}</Text>
<Text style={[styles.metaText, { color: currentTheme.colors.text }]}>{metadata.certification}</Text>
)}
{metadata.imdbRating && (
<View style={styles.ratingContainer}>
@ -47,7 +48,7 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
style={styles.imdbLogo}
contentFit="contain"
/>
<Text style={styles.ratingText}>{metadata.imdbRating}</Text>
<Text style={[styles.ratingText, { color: currentTheme.colors.text }]}>{metadata.imdbRating}</Text>
</View>
)}
</View>
@ -59,14 +60,14 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
>
{metadata.directors && metadata.directors.length > 0 && (
<View style={styles.creatorSection}>
<Text style={styles.creatorLabel}>Director{metadata.directors.length > 1 ? 's' : ''}:</Text>
<Text style={styles.creatorText}>{metadata.directors.join(', ')}</Text>
<Text style={[styles.creatorLabel, { color: currentTheme.colors.white }]}>Director{metadata.directors.length > 1 ? 's' : ''}:</Text>
<Text style={[styles.creatorText, { color: currentTheme.colors.mediumEmphasis }]}>{metadata.directors.join(', ')}</Text>
</View>
)}
{metadata.creators && metadata.creators.length > 0 && (
<View style={styles.creatorSection}>
<Text style={styles.creatorLabel}>Creator{metadata.creators.length > 1 ? 's' : ''}:</Text>
<Text style={styles.creatorText}>{metadata.creators.join(', ')}</Text>
<Text style={[styles.creatorLabel, { color: currentTheme.colors.white }]}>Creator{metadata.creators.length > 1 ? 's' : ''}:</Text>
<Text style={[styles.creatorText, { color: currentTheme.colors.mediumEmphasis }]}>{metadata.creators.join(', ')}</Text>
</View>
)}
</Animated.View>
@ -81,17 +82,17 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
onPress={() => setIsFullDescriptionOpen(!isFullDescriptionOpen)}
activeOpacity={0.7}
>
<Text style={styles.description} numberOfLines={isFullDescriptionOpen ? undefined : 3}>
<Text style={[styles.description, { color: currentTheme.colors.mediumEmphasis }]} numberOfLines={isFullDescriptionOpen ? undefined : 3}>
{metadata.description}
</Text>
<View style={styles.showMoreButton}>
<Text style={styles.showMoreText}>
<Text style={[styles.showMoreText, { color: currentTheme.colors.textMuted }]}>
{isFullDescriptionOpen ? 'Show Less' : 'Show More'}
</Text>
<MaterialIcons
name={isFullDescriptionOpen ? "keyboard-arrow-up" : "keyboard-arrow-down"}
size={18}
color={colors.textMuted}
color={currentTheme.colors.textMuted}
/>
</View>
</TouchableOpacity>
@ -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,
},

View file

@ -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<MoreLikeThisSectionProps> = ({
recommendations,
loadingRecommendations
}) => {
const { currentTheme } = useTheme();
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const handleItemPress = async (item: StreamingContent) => {
@ -69,11 +70,11 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
>
<Image
source={{ uri: item.poster }}
style={styles.poster}
style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1 }]}
contentFit="cover"
transition={200}
/>
<Text style={styles.title} numberOfLines={2}>
<Text style={[styles.title, { color: currentTheme.colors.mediumEmphasis }]} numberOfLines={2}>
{item.name}
</Text>
</TouchableOpacity>
@ -82,7 +83,7 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
if (loadingRecommendations) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="small" color={colors.primary} />
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
</View>
);
}
@ -93,7 +94,7 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
return (
<View style={styles.container}>
<Text style={styles.sectionTitle}>More Like This</Text>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>More Like This</Text>
<FlatList
data={recommendations}
renderItem={renderItem}
@ -115,7 +116,6 @@ const styles = StyleSheet.create({
sectionTitle: {
fontSize: 20,
fontWeight: '800',
color: colors.highEmphasis,
marginBottom: 12,
marginTop: 8,
paddingHorizontal: 16,
@ -132,12 +132,10 @@ const styles = StyleSheet.create({
width: POSTER_WIDTH,
height: POSTER_HEIGHT,
borderRadius: 8,
backgroundColor: colors.elevation1,
marginBottom: 8,
},
title: {
fontSize: 13,
color: colors.mediumEmphasis,
fontWeight: '500',
lineHeight: 18,
},

View file

@ -1,6 +1,6 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { colors } from '../../styles/colors';
import { useTheme } from '../../contexts/ThemeContext';
import { StreamingContent } from '../../types/metadata';
interface MovieContentProps {
@ -8,6 +8,7 @@ interface MovieContentProps {
}
export const MovieContent: React.FC<MovieContentProps> = ({ 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<MovieContentProps> = ({ metadata }) => {
<View style={styles.additionalInfo}>
{metadata.director && (
<View style={styles.metadataRow}>
<Text style={styles.metadataLabel}>Director:</Text>
<Text style={styles.metadataValue}>{metadata.director}</Text>
<Text style={[styles.metadataLabel, { color: currentTheme.colors.textMuted }]}>Director:</Text>
<Text style={[styles.metadataValue, { color: currentTheme.colors.text }]}>{metadata.director}</Text>
</View>
)}
{metadata.writer && (
<View style={styles.metadataRow}>
<Text style={styles.metadataLabel}>Writer:</Text>
<Text style={styles.metadataValue}>{metadata.writer}</Text>
<Text style={[styles.metadataLabel, { color: currentTheme.colors.textMuted }]}>Writer:</Text>
<Text style={[styles.metadataValue, { color: currentTheme.colors.text }]}>{metadata.writer}</Text>
</View>
)}
{hasCast && (
<View style={styles.metadataRow}>
<Text style={styles.metadataLabel}>Cast:</Text>
<Text style={styles.metadataValue}>{castDisplay}</Text>
<Text style={[styles.metadataLabel, { color: currentTheme.colors.textMuted }]}>Cast:</Text>
<Text style={[styles.metadataValue, { color: currentTheme.colors.text }]}>{castDisplay}</Text>
</View>
)}
</View>
@ -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,

View file

@ -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<RatingsSectionProps> = ({ imdbId, type })
const [enabledProviders, setEnabledProviders] = useState<Record<string, boolean>>({});
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<RatingsSectionProps> = ({ imdbId, type })
if (loading) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="small" color={colors.primary} />
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
</View>
);
}
@ -214,86 +215,128 @@ export const RatingsSection: React.FC<RatingsSectionProps> = ({ 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 (
<View key={source} style={styles.ratingItem}>
{config.isImage ? (
<Image
source={config.icon}
style={styles.ratingIcon}
resizeMode="contain"
/>
) : (
<config.icon
width={16}
height={16}
style={styles.ratingIcon}
/>
)}
<Text style={[styles.ratingValue, {color: config.color}]}>
{displayValue}{config.suffix}
</Text>
</View>
);
})}
<View style={styles.header}>
<Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]}>Ratings</Text>
</View>
<View style={styles.ratingsContainer}>
{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 (
<View key={source} style={styles.ratingItem}>
<View style={styles.ratingIconContainer}>
{config.isImage ? (
<Image
source={config.icon as any}
style={styles.ratingIconImage}
resizeMode="contain"
/>
) : (
<View style={styles.svgContainer}>
{React.createElement(config.icon as any, {
width: 24,
height: 24,
})}
</View>
)}
</View>
<Text
style={[
styles.ratingValue,
{ color: config.color }
]}
>
{config.prefix}{displayValue}{config.suffix}
</Text>
<Text style={[styles.ratingSource, { color: currentTheme.colors.mediumEmphasis }]}>{getSourceLabel(source)}</Text>
</View>
);
})}
</View>
</Animated.View>
);
};
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,
},
});

View file

@ -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 (
<SafeAreaView
style={[styles.container, { backgroundColor: colors.darkBackground }]}
style={[styles.container, {
backgroundColor: currentTheme.colors.darkBackground
}]}
edges={['bottom']}
>
<StatusBar
@ -191,8 +196,10 @@ const MetadataScreen = () => {
barStyle="light-content"
/>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={[styles.loadingText, { color: colors.lightGray }]}>
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
<Text style={[styles.loadingText, {
color: currentTheme.colors.mediumEmphasis
}]}>
Loading content...
</Text>
</View>
@ -203,7 +210,9 @@ const MetadataScreen = () => {
if (metadataError || !metadata) {
return (
<SafeAreaView
style={[styles.container, { backgroundColor: colors.darkBackground }]}
style={[styles.container, {
backgroundColor: currentTheme.colors.darkBackground
}]}
edges={['bottom']}
>
<StatusBar
@ -215,22 +224,24 @@ const MetadataScreen = () => {
<MaterialIcons
name="error-outline"
size={64}
color={colors.textMuted}
color={currentTheme.colors.textMuted}
/>
<Text style={[styles.errorText, { color: colors.text }]}>
<Text style={[styles.errorText, {
color: currentTheme.colors.highEmphasis
}]}>
{metadataError || 'Content not found'}
</Text>
<TouchableOpacity
style={[
styles.retryButton,
{ backgroundColor: colors.primary }
{ backgroundColor: currentTheme.colors.primary }
]}
onPress={loadMetadata}
>
<MaterialIcons
name="refresh"
size={20}
color={colors.white}
color={currentTheme.colors.white}
style={{ marginRight: 8 }}
/>
<Text style={styles.retryButtonText}>Try Again</Text>
@ -238,11 +249,11 @@ const MetadataScreen = () => {
<TouchableOpacity
style={[
styles.backButton,
{ borderColor: colors.primary }
{ borderColor: currentTheme.colors.primary }
]}
onPress={handleBack}
>
<Text style={[styles.backButtonText, { color: colors.primary }]}>
<Text style={[styles.backButtonText, { color: currentTheme.colors.primary }]}>
Go Back
</Text>
</TouchableOpacity>
@ -253,7 +264,9 @@ const MetadataScreen = () => {
return (
<SafeAreaView
style={[styles.container, { backgroundColor: colors.darkBackground }]}
style={[containerAnimatedStyle, styles.container, {
backgroundColor: currentTheme.colors.darkBackground
}]}
edges={['bottom']}
>
<StatusBar
@ -386,7 +399,6 @@ const styles = StyleSheet.create({
loadingText: {
marginTop: 16,
fontSize: 16,
textAlign: 'center',
},
errorContainer: {
flex: 1,
@ -395,11 +407,10 @@ const styles = StyleSheet.create({
padding: 32,
},
errorText: {
fontSize: 16,
fontSize: 18,
textAlign: 'center',
marginTop: 16,
marginBottom: 24,
lineHeight: 24,
},
retryButton: {
flexDirection: 'row',
@ -411,7 +422,6 @@ const styles = StyleSheet.create({
marginBottom: 16,
},
retryButtonText: {
color: colors.white,
fontSize: 16,
fontWeight: '600',
},