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, View,
Text, Text,
StyleSheet, StyleSheet,
FlatList,
TouchableOpacity, TouchableOpacity,
ActivityIndicator, ActivityIndicator,
ScrollView,
} from 'react-native'; } from 'react-native';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import Animated, {
import { colors } from '../../styles/colors'; FadeIn,
import { Cast } from '../../types/metadata'; Layout,
import { tmdbService } from '../../services/tmdbService'; } from 'react-native-reanimated';
import { useTheme } from '../../contexts/ThemeContext';
interface CastSectionProps { interface CastSectionProps {
cast: Cast[]; cast: any[];
loadingCast: boolean; loadingCast: boolean;
onSelectCastMember: (member: Cast) => void; onSelectCastMember: (castMember: any) => void;
} }
export const CastSection: React.FC<CastSectionProps> = ({ export const CastSection: React.FC<CastSectionProps> = ({
@ -24,123 +25,137 @@ export const CastSection: React.FC<CastSectionProps> = ({
loadingCast, loadingCast,
onSelectCastMember, onSelectCastMember,
}) => { }) => {
const { currentTheme } = useTheme();
if (loadingCast) { if (loadingCast) {
return ( return (
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
<ActivityIndicator size="small" color={colors.primary} /> <ActivityIndicator size="small" color={currentTheme.colors.primary} />
</View> </View>
); );
} }
if (!cast.length) { if (!cast || cast.length === 0) {
return null; return null;
} }
return ( return (
<View style={styles.castSection}> <Animated.View
<Text style={styles.sectionTitle}>Cast</Text> style={styles.castSection}
<ScrollView entering={FadeIn.duration(500).delay(300)}
horizontal layout={Layout}
>
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>Cast</Text>
</View>
<FlatList
horizontal
data={cast}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
style={styles.castScrollContainer} contentContainerStyle={styles.castList}
contentContainerStyle={styles.castContainer} keyExtractor={(item) => item.id.toString()}
snapToAlignment="start" renderItem={({ item, index }) => (
> <Animated.View
{cast.map((member) => ( entering={FadeIn.duration(500).delay(100 + index * 50)}
<TouchableOpacity layout={Layout}
key={member.id}
style={styles.castMember}
onPress={() => onSelectCastMember(member)}
> >
<View style={styles.castImageContainer}> <TouchableOpacity
{member.profile_path ? ( style={styles.castCard}
<Image onPress={() => onSelectCastMember(item)}
source={{ activeOpacity={0.7}
uri: `https://image.tmdb.org/t/p/w185${member.profile_path}` >
}} <View style={styles.castImageContainer}>
style={styles.castImage} {item.profile_path ? (
contentFit="cover" <Image
/> source={{
) : ( uri: `https://image.tmdb.org/t/p/w185${item.profile_path}`,
<MaterialIcons }}
name="person" style={styles.castImage}
size={32} contentFit="cover"
color={colors.textMuted} 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> </TouchableOpacity>
<View style={styles.castTextContainer}> </Animated.View>
<Text style={styles.castName} numberOfLines={1}>{member.name}</Text> )}
<Text style={styles.castCharacter} numberOfLines={1}>{member.character}</Text> />
</View> </Animated.View>
</TouchableOpacity>
))}
</ScrollView>
</View>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
castSection: {
marginBottom: 24,
paddingHorizontal: 0,
},
loadingContainer: { loadingContainer: {
paddingVertical: 20,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
padding: 12,
}, },
castSection: { sectionHeader: {
marginTop: 0, flexDirection: 'row',
paddingLeft: 0, alignItems: 'center',
}, justifyContent: 'space-between',
sectionTitle: { marginBottom: 12,
color: colors.highEmphasis,
fontSize: 18,
fontWeight: '700',
marginBottom: 10,
paddingHorizontal: 16, paddingHorizontal: 16,
}, },
castScrollContainer: { sectionTitle: {
marginTop: 4, fontSize: 18,
fontWeight: '700',
}, },
castContainer: { castList: {
paddingHorizontal: 12, paddingHorizontal: 16,
paddingVertical: 4, paddingBottom: 4,
}, },
castMember: { castCard: {
width: 80, marginRight: 16,
marginRight: 12, width: 90,
alignItems: 'center', alignItems: 'center',
}, },
castImageContainer: { castImageContainer: {
width: 64, width: 80,
height: 64, height: 80,
borderRadius: 32, borderRadius: 40,
backgroundColor: colors.elevation2,
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden', overflow: 'hidden',
marginBottom: 6, marginBottom: 8,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.1)',
}, },
castImage: { castImage: {
width: 64,
height: 64,
borderRadius: 32,
},
castTextContainer: {
width: '100%', width: '100%',
height: '100%',
},
castImagePlaceholder: {
width: '100%',
height: '100%',
borderRadius: 40,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
},
placeholderText: {
fontSize: 24,
fontWeight: '600',
}, },
castName: { castName: {
color: colors.highEmphasis, fontSize: 14,
fontSize: 13,
fontWeight: '600', fontWeight: '600',
textAlign: 'center', textAlign: 'center',
width: 90,
}, },
castCharacter: { characterName: {
color: colors.mediumEmphasis,
fontSize: 12, fontSize: 12,
textAlign: 'center', textAlign: 'center',
width: 90,
marginTop: 2, marginTop: 2,
opacity: 0.8,
}, },
}); });

View file

@ -16,7 +16,7 @@ import Animated, {
interpolate, interpolate,
Extrapolate, Extrapolate,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { colors } from '../../styles/colors'; import { useTheme } from '../../contexts/ThemeContext';
import { logger } from '../../utils/logger'; import { logger } from '../../utils/logger';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
@ -46,6 +46,8 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
safeAreaTop, safeAreaTop,
setLogoLoadError, setLogoLoadError,
}) => { }) => {
const { currentTheme } = useTheme();
// Animated styles for the header // Animated styles for the header
const headerAnimatedStyle = useAnimatedStyle(() => ({ const headerAnimatedStyle = useAnimatedStyle(() => ({
opacity: headerOpacity.value, opacity: headerOpacity.value,
@ -74,7 +76,11 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
onPress={handleBack} onPress={handleBack}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} 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> </TouchableOpacity>
<View style={styles.headerTitleContainer}> <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> </View>
@ -102,7 +108,7 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
<MaterialIcons <MaterialIcons
name={inLibrary ? 'bookmark' : 'bookmark-border'} name={inLibrary ? 'bookmark' : 'bookmark-border'}
size={22} size={22}
color={colors.highEmphasis} color={currentTheme.colors.highEmphasis}
/> />
</TouchableOpacity> </TouchableOpacity>
</Animated.View> </Animated.View>
@ -121,7 +127,11 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
onPress={handleBack} onPress={handleBack}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} 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> </TouchableOpacity>
<View style={styles.headerTitleContainer}> <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> </View>
@ -149,13 +159,13 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
<MaterialIcons <MaterialIcons
name={inLibrary ? 'bookmark' : 'bookmark-border'} name={inLibrary ? 'bookmark' : 'bookmark-border'}
size={22} size={22}
color={colors.highEmphasis} color={currentTheme.colors.highEmphasis}
/> />
</TouchableOpacity> </TouchableOpacity>
</Animated.View> </Animated.View>
</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> </Animated.View>
); );
}; };
@ -190,7 +200,6 @@ const styles = StyleSheet.create({
left: 0, left: 0,
right: 0, right: 0,
height: 0.5, height: 0.5,
backgroundColor: 'rgba(255,255,255,0.15)',
}, },
headerTitleContainer: { headerTitleContainer: {
flex: 1, flex: 1,
@ -218,7 +227,6 @@ const styles = StyleSheet.create({
maxWidth: 240, maxWidth: 240,
}, },
floatingHeaderTitle: { floatingHeaderTitle: {
color: colors.highEmphasis,
fontSize: 18, fontSize: 18,
fontWeight: '700', fontWeight: '700',
textAlign: 'center', textAlign: 'center',

View file

@ -14,7 +14,7 @@ import Animated, {
interpolate, interpolate,
Extrapolate, Extrapolate,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { colors } from '../../styles/colors'; import { useTheme } from '../../contexts/ThemeContext';
import { logger } from '../../utils/logger'; import { logger } from '../../utils/logger';
import { TMDBService } from '../../services/tmdbService'; import { TMDBService } from '../../services/tmdbService';
@ -77,6 +77,7 @@ const ActionButtons = React.memo(({
playButtonText: string; playButtonText: string;
animatedStyle: any; animatedStyle: any;
}) => { }) => {
const { currentTheme } = useTheme();
return ( return (
<Animated.View style={[styles.actionButtons, animatedStyle]}> <Animated.View style={[styles.actionButtons, animatedStyle]}>
<TouchableOpacity <TouchableOpacity
@ -100,7 +101,7 @@ const ActionButtons = React.memo(({
<MaterialIcons <MaterialIcons
name={inLibrary ? 'bookmark' : 'bookmark-border'} name={inLibrary ? 'bookmark' : 'bookmark-border'}
size={24} size={24}
color="#fff" color={currentTheme.colors.white}
/> />
<Text style={styles.infoButtonText}> <Text style={styles.infoButtonText}>
{inLibrary ? 'Saved' : 'Save'} {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> </TouchableOpacity>
)} )}
</Animated.View> </Animated.View>
@ -174,6 +179,7 @@ const WatchProgressDisplay = React.memo(({
getEpisodeDetails: (episodeId: string) => { seasonNumber: string; episodeNumber: string; episodeName: string } | null; getEpisodeDetails: (episodeId: string) => { seasonNumber: string; episodeNumber: string; episodeName: string } | null;
animatedStyle: any; animatedStyle: any;
}) => { }) => {
const { currentTheme } = useTheme();
if (!watchProgress || watchProgress.duration === 0) { if (!watchProgress || watchProgress.duration === 0) {
return null; return null;
} }
@ -195,11 +201,14 @@ const WatchProgressDisplay = React.memo(({
<View <View
style={[ style={[
styles.watchProgressFill, styles.watchProgressFill,
{ width: `${progressPercent}%` } {
width: `${progressPercent}%`,
backgroundColor: currentTheme.colors.primary
}
]} ]}
/> />
</View> </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} {progressPercent >= 95 ? 'Watched' : `${Math.round(progressPercent)}% watched`}{episodeInfo} Last watched on {formattedTime}
</Text> </Text>
</Animated.View> </Animated.View>
@ -236,11 +245,12 @@ const HeroSection: React.FC<HeroSectionProps> = ({
setBannerImage, setBannerImage,
setLogoLoadError, setLogoLoadError,
}) => { }) => {
const { currentTheme } = useTheme();
// Animated styles // Animated styles
const heroAnimatedStyle = useAnimatedStyle(() => ({ const heroAnimatedStyle = useAnimatedStyle(() => ({
width: '100%', width: '100%',
height: heroHeight.value, height: heroHeight.value,
backgroundColor: colors.black, backgroundColor: currentTheme.colors.black,
transform: [{ scale: heroScale.value }], transform: [{ scale: heroScale.value }],
opacity: heroOpacity.value, opacity: heroOpacity.value,
})); }));
@ -309,9 +319,13 @@ const HeroSection: React.FC<HeroSectionProps> = ({
return genresToDisplay.slice(0, 4).map((genreName, index, array) => ( return genresToDisplay.slice(0, 4).map((genreName, index, array) => (
<React.Fragment key={index}> <React.Fragment key={index}>
<Text style={styles.genreText}>{genreName}</Text> <Text style={[styles.genreText, { color: currentTheme.colors.text }]}>
{genreName}
</Text>
{index < array.length - 1 && ( {index < array.length - 1 && (
<Text style={styles.genreDot}></Text> <Text style={[styles.genreDot, { color: currentTheme.colors.text, opacity: 0.6 }]}>
</Text>
)} )}
</React.Fragment> </React.Fragment>
)); ));
@ -321,7 +335,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({
<Animated.View style={heroAnimatedStyle}> <Animated.View style={heroAnimatedStyle}>
<View style={styles.heroSection}> <View style={styles.heroSection}>
{loadingBanner ? ( {loadingBanner ? (
<View style={[styles.absoluteFill, { backgroundColor: colors.black }]} /> <View style={[styles.absoluteFill, { backgroundColor: currentTheme.colors.black }]} />
) : ( ) : (
<Animated.Image <Animated.Image
source={{ uri: bannerImage || metadata.banner || metadata.poster }} source={{ uri: bannerImage || metadata.banner || metadata.poster }}
@ -337,12 +351,12 @@ const HeroSection: React.FC<HeroSectionProps> = ({
)} )}
<LinearGradient <LinearGradient
colors={[ colors={[
`${colors.darkBackground}00`, `${currentTheme.colors.darkBackground}00`,
`${colors.darkBackground}20`, `${currentTheme.colors.darkBackground}20`,
`${colors.darkBackground}50`, `${currentTheme.colors.darkBackground}50`,
`${colors.darkBackground}C0`, `${currentTheme.colors.darkBackground}C0`,
`${colors.darkBackground}F8`, `${currentTheme.colors.darkBackground}F8`,
colors.darkBackground currentTheme.colors.darkBackground
]} ]}
locations={[0, 0.4, 0.65, 0.8, 0.9, 1]} locations={[0, 0.4, 0.65, 0.8, 0.9, 1]}
style={styles.heroGradient} 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> </Animated.View>
</View> </View>
@ -405,7 +419,7 @@ const styles = StyleSheet.create({
heroSection: { heroSection: {
width: '100%', width: '100%',
height: height * 0.5, height: height * 0.5,
backgroundColor: colors.black, backgroundColor: '#000',
overflow: 'hidden', overflow: 'hidden',
}, },
absoluteFill: { absoluteFill: {
@ -442,7 +456,6 @@ const styles = StyleSheet.create({
alignSelf: 'center', alignSelf: 'center',
}, },
heroTitle: { heroTitle: {
color: colors.highEmphasis,
fontSize: 28, fontSize: 28,
fontWeight: '900', fontWeight: '900',
marginBottom: 12, marginBottom: 12,
@ -461,15 +474,12 @@ const styles = StyleSheet.create({
gap: 4, gap: 4,
}, },
genreText: { genreText: {
color: colors.text,
fontSize: 12, fontSize: 12,
fontWeight: '500', fontWeight: '500',
}, },
genreDot: { genreDot: {
color: colors.text,
fontSize: 12, fontSize: 12,
fontWeight: '500', fontWeight: '500',
opacity: 0.6,
marginHorizontal: 4, marginHorizontal: 4,
}, },
actionButtons: { actionButtons: {
@ -494,7 +504,7 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
}, },
playButton: { playButton: {
backgroundColor: colors.white, backgroundColor: '#fff',
}, },
infoButton: { infoButton: {
backgroundColor: 'rgba(255,255,255,0.2)', backgroundColor: 'rgba(255,255,255,0.2)',
@ -546,11 +556,9 @@ const styles = StyleSheet.create({
}, },
watchProgressFill: { watchProgressFill: {
height: '100%', height: '100%',
backgroundColor: colors.primary,
borderRadius: 1.5, borderRadius: 1.5,
}, },
watchProgressText: { watchProgressText: {
color: colors.textMuted,
fontSize: 12, fontSize: 12,
textAlign: 'center', textAlign: 'center',
opacity: 0.9, opacity: 0.9,

View file

@ -12,7 +12,7 @@ import Animated, {
Easing, Easing,
FadeIn, FadeIn,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { colors } from '../../styles/colors'; import { useTheme } from '../../contexts/ThemeContext';
interface MetadataDetailsProps { interface MetadataDetailsProps {
metadata: any; metadata: any;
@ -25,6 +25,7 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
imdbId, imdbId,
type, type,
}) => { }) => {
const { currentTheme } = useTheme();
const [isFullDescriptionOpen, setIsFullDescriptionOpen] = useState(false); const [isFullDescriptionOpen, setIsFullDescriptionOpen] = useState(false);
return ( return (
@ -32,13 +33,13 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
{/* Meta Info */} {/* Meta Info */}
<View style={styles.metaInfo}> <View style={styles.metaInfo}>
{metadata.year && ( {metadata.year && (
<Text style={styles.metaText}>{metadata.year}</Text> <Text style={[styles.metaText, { color: currentTheme.colors.text }]}>{metadata.year}</Text>
)} )}
{metadata.runtime && ( {metadata.runtime && (
<Text style={styles.metaText}>{metadata.runtime}</Text> <Text style={[styles.metaText, { color: currentTheme.colors.text }]}>{metadata.runtime}</Text>
)} )}
{metadata.certification && ( {metadata.certification && (
<Text style={styles.metaText}>{metadata.certification}</Text> <Text style={[styles.metaText, { color: currentTheme.colors.text }]}>{metadata.certification}</Text>
)} )}
{metadata.imdbRating && ( {metadata.imdbRating && (
<View style={styles.ratingContainer}> <View style={styles.ratingContainer}>
@ -47,7 +48,7 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
style={styles.imdbLogo} style={styles.imdbLogo}
contentFit="contain" contentFit="contain"
/> />
<Text style={styles.ratingText}>{metadata.imdbRating}</Text> <Text style={[styles.ratingText, { color: currentTheme.colors.text }]}>{metadata.imdbRating}</Text>
</View> </View>
)} )}
</View> </View>
@ -59,14 +60,14 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
> >
{metadata.directors && metadata.directors.length > 0 && ( {metadata.directors && metadata.directors.length > 0 && (
<View style={styles.creatorSection}> <View style={styles.creatorSection}>
<Text style={styles.creatorLabel}>Director{metadata.directors.length > 1 ? 's' : ''}:</Text> <Text style={[styles.creatorLabel, { color: currentTheme.colors.white }]}>Director{metadata.directors.length > 1 ? 's' : ''}:</Text>
<Text style={styles.creatorText}>{metadata.directors.join(', ')}</Text> <Text style={[styles.creatorText, { color: currentTheme.colors.mediumEmphasis }]}>{metadata.directors.join(', ')}</Text>
</View> </View>
)} )}
{metadata.creators && metadata.creators.length > 0 && ( {metadata.creators && metadata.creators.length > 0 && (
<View style={styles.creatorSection}> <View style={styles.creatorSection}>
<Text style={styles.creatorLabel}>Creator{metadata.creators.length > 1 ? 's' : ''}:</Text> <Text style={[styles.creatorLabel, { color: currentTheme.colors.white }]}>Creator{metadata.creators.length > 1 ? 's' : ''}:</Text>
<Text style={styles.creatorText}>{metadata.creators.join(', ')}</Text> <Text style={[styles.creatorText, { color: currentTheme.colors.mediumEmphasis }]}>{metadata.creators.join(', ')}</Text>
</View> </View>
)} )}
</Animated.View> </Animated.View>
@ -81,17 +82,17 @@ const MetadataDetails: React.FC<MetadataDetailsProps> = ({
onPress={() => setIsFullDescriptionOpen(!isFullDescriptionOpen)} onPress={() => setIsFullDescriptionOpen(!isFullDescriptionOpen)}
activeOpacity={0.7} 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} {metadata.description}
</Text> </Text>
<View style={styles.showMoreButton}> <View style={styles.showMoreButton}>
<Text style={styles.showMoreText}> <Text style={[styles.showMoreText, { color: currentTheme.colors.textMuted }]}>
{isFullDescriptionOpen ? 'Show Less' : 'Show More'} {isFullDescriptionOpen ? 'Show Less' : 'Show More'}
</Text> </Text>
<MaterialIcons <MaterialIcons
name={isFullDescriptionOpen ? "keyboard-arrow-up" : "keyboard-arrow-down"} name={isFullDescriptionOpen ? "keyboard-arrow-up" : "keyboard-arrow-down"}
size={18} size={18}
color={colors.textMuted} color={currentTheme.colors.textMuted}
/> />
</View> </View>
</TouchableOpacity> </TouchableOpacity>
@ -110,7 +111,6 @@ const styles = StyleSheet.create({
marginBottom: 12, marginBottom: 12,
}, },
metaText: { metaText: {
color: colors.text,
fontSize: 15, fontSize: 15,
fontWeight: '700', fontWeight: '700',
letterSpacing: 0.3, letterSpacing: 0.3,
@ -127,7 +127,6 @@ const styles = StyleSheet.create({
marginRight: 4, marginRight: 4,
}, },
ratingText: { ratingText: {
color: colors.text,
fontWeight: '700', fontWeight: '700',
fontSize: 15, fontSize: 15,
letterSpacing: 0.3, letterSpacing: 0.3,
@ -143,14 +142,12 @@ const styles = StyleSheet.create({
height: 20 height: 20
}, },
creatorLabel: { creatorLabel: {
color: colors.white,
fontSize: 14, fontSize: 14,
fontWeight: '600', fontWeight: '600',
marginRight: 8, marginRight: 8,
lineHeight: 20 lineHeight: 20
}, },
creatorText: { creatorText: {
color: colors.lightGray,
fontSize: 14, fontSize: 14,
flex: 1, flex: 1,
lineHeight: 20 lineHeight: 20
@ -160,7 +157,6 @@ const styles = StyleSheet.create({
paddingHorizontal: 16, paddingHorizontal: 16,
}, },
description: { description: {
color: colors.mediumEmphasis,
fontSize: 15, fontSize: 15,
lineHeight: 24, lineHeight: 24,
}, },
@ -171,7 +167,6 @@ const styles = StyleSheet.create({
paddingVertical: 4, paddingVertical: 4,
}, },
showMoreText: { showMoreText: {
color: colors.textMuted,
fontSize: 14, fontSize: 14,
marginRight: 4, marginRight: 4,
}, },

View file

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

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { View, Text, StyleSheet } from 'react-native'; import { View, Text, StyleSheet } from 'react-native';
import { colors } from '../../styles/colors'; import { useTheme } from '../../contexts/ThemeContext';
import { StreamingContent } from '../../types/metadata'; import { StreamingContent } from '../../types/metadata';
interface MovieContentProps { interface MovieContentProps {
@ -8,6 +8,7 @@ interface MovieContentProps {
} }
export const MovieContent: React.FC<MovieContentProps> = ({ metadata }) => { export const MovieContent: React.FC<MovieContentProps> = ({ metadata }) => {
const { currentTheme } = useTheme();
const hasCast = Array.isArray(metadata.cast) && metadata.cast.length > 0; const hasCast = Array.isArray(metadata.cast) && metadata.cast.length > 0;
const castDisplay = hasCast ? (metadata.cast as string[]).slice(0, 5).join(', ') : ''; 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}> <View style={styles.additionalInfo}>
{metadata.director && ( {metadata.director && (
<View style={styles.metadataRow}> <View style={styles.metadataRow}>
<Text style={styles.metadataLabel}>Director:</Text> <Text style={[styles.metadataLabel, { color: currentTheme.colors.textMuted }]}>Director:</Text>
<Text style={styles.metadataValue}>{metadata.director}</Text> <Text style={[styles.metadataValue, { color: currentTheme.colors.text }]}>{metadata.director}</Text>
</View> </View>
)} )}
{metadata.writer && ( {metadata.writer && (
<View style={styles.metadataRow}> <View style={styles.metadataRow}>
<Text style={styles.metadataLabel}>Writer:</Text> <Text style={[styles.metadataLabel, { color: currentTheme.colors.textMuted }]}>Writer:</Text>
<Text style={styles.metadataValue}>{metadata.writer}</Text> <Text style={[styles.metadataValue, { color: currentTheme.colors.text }]}>{metadata.writer}</Text>
</View> </View>
)} )}
{hasCast && ( {hasCast && (
<View style={styles.metadataRow}> <View style={styles.metadataRow}>
<Text style={styles.metadataLabel}>Cast:</Text> <Text style={[styles.metadataLabel, { color: currentTheme.colors.textMuted }]}>Cast:</Text>
<Text style={styles.metadataValue}>{castDisplay}</Text> <Text style={[styles.metadataValue, { color: currentTheme.colors.text }]}>{castDisplay}</Text>
</View> </View>
)} )}
</View> </View>
@ -53,12 +54,10 @@ const styles = StyleSheet.create({
alignItems: 'flex-start', alignItems: 'flex-start',
}, },
metadataLabel: { metadataLabel: {
color: colors.textMuted,
fontSize: 15, fontSize: 15,
width: 70, width: 70,
}, },
metadataValue: { metadataValue: {
color: colors.text,
fontSize: 15, fontSize: 15,
flex: 1, flex: 1,
lineHeight: 24, lineHeight: 24,

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { View, Text, StyleSheet, ActivityIndicator, Image, Animated } from 'react-native'; 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 { useMDBListRatings } from '../../hooks/useMDBListRatings';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { isMDBListEnabled, RATING_PROVIDERS_STORAGE_KEY } from '../../screens/MDBListSettingsScreen'; 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 [enabledProviders, setEnabledProviders] = useState<Record<string, boolean>>({});
const [isMDBEnabled, setIsMDBEnabled] = useState(true); const [isMDBEnabled, setIsMDBEnabled] = useState(true);
const fadeAnim = useRef(new Animated.Value(0)).current; const fadeAnim = useRef(new Animated.Value(0)).current;
const { currentTheme } = useTheme();
useEffect(() => { useEffect(() => {
loadProviderSettings(); loadProviderSettings();
@ -120,7 +121,7 @@ export const RatingsSection: React.FC<RatingsSectionProps> = ({ imdbId, type })
if (loading) { if (loading) {
return ( return (
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
<ActivityIndicator size="small" color={colors.primary} /> <ActivityIndicator size="small" color={currentTheme.colors.primary} />
</View> </View>
); );
} }
@ -214,86 +215,128 @@ export const RatingsSection: React.FC<RatingsSectionProps> = ({ imdbId, type })
}, },
]} ]}
> >
{displayRatings.map(([source, value]) => { <View style={styles.header}>
const config = ratingConfig[source as keyof typeof ratingConfig]; <Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]}>Ratings</Text>
const numericValue = typeof value === 'string' ? parseFloat(value) : value; </View>
const displayValue = config.transform(numericValue); <View style={styles.ratingsContainer}>
{displayRatings.map(([source, value]) => {
// Get a short display name for the rating source const config = ratingConfig[source as keyof typeof ratingConfig];
const getSourceLabel = (src: string): string => { const displayValue = config.transform(parseFloat(value as string));
switch(src) {
case 'imdb': return 'IMDb'; // Get a short display name for the rating source
case 'tmdb': return 'TMDB'; const getSourceLabel = (src: string): string => {
case 'tomatoes': return 'RT'; switch(src) {
case 'audience': return 'Aud'; case 'imdb': return 'IMDb';
case 'metacritic': return 'Meta'; case 'tmdb': return 'TMDB';
case 'letterboxd': return 'LBXD'; case 'tomatoes': return 'RT';
case 'trakt': return 'Trakt'; case 'audience': return 'Aud';
default: return src; 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 return (
source={config.icon} <View key={source} style={styles.ratingItem}>
style={styles.ratingIcon} <View style={styles.ratingIconContainer}>
resizeMode="contain" {config.isImage ? (
/> <Image
) : ( source={config.icon as any}
<config.icon style={styles.ratingIconImage}
width={16} resizeMode="contain"
height={16} />
style={styles.ratingIcon} ) : (
/> <View style={styles.svgContainer}>
)} {React.createElement(config.icon as any, {
<Text style={[styles.ratingValue, {color: config.color}]}> width: 24,
{displayValue}{config.suffix} height: 24,
</Text> })}
</View> </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> </Animated.View>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flexDirection: 'row', marginBottom: 20,
alignItems: 'center', paddingHorizontal: 16,
justifyContent: 'center',
marginTop: 8,
marginBottom: 16,
paddingHorizontal: 12,
gap: 4,
}, },
loadingContainer: { loadingContainer: {
alignItems: 'center', height: 80,
justifyContent: 'center', justifyContent: 'center',
height: 40, alignItems: 'center',
marginVertical: 16,
}, },
ratingItem: { header: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.4)', justifyContent: 'space-between',
paddingVertical: 3, marginBottom: 12,
paddingHorizontal: 4,
borderRadius: 4,
}, },
ratingIcon: { title: {
width: 16, fontSize: 18,
height: 16, fontWeight: '700',
marginRight: 3, },
alignSelf: 'center', 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: { ratingValue: {
fontSize: 13, fontSize: 16,
fontWeight: 'bold', fontWeight: '700',
marginVertical: 2,
}, },
ratingLabel: { ratingSource: {
fontSize: 11, 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 { useRoute, useNavigation } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import { colors } from '../styles/colors'; import { useTheme } from '../contexts/ThemeContext';
import { useMetadata } from '../hooks/useMetadata'; import { useMetadata } from '../hooks/useMetadata';
import { CastSection } from '../components/metadata/CastSection'; import { CastSection } from '../components/metadata/CastSection';
import { SeriesContent } from '../components/metadata/SeriesContent'; import { SeriesContent } from '../components/metadata/SeriesContent';
@ -48,6 +48,9 @@ const MetadataScreen = () => {
// Add settings hook // Add settings hook
const { settings } = useSettings(); const { settings } = useSettings();
// Get theme context
const { currentTheme } = useTheme();
// Get safe area insets // Get safe area insets
const { top: safeAreaTop } = useSafeAreaInsets(); const { top: safeAreaTop } = useSafeAreaInsets();
@ -182,7 +185,9 @@ const MetadataScreen = () => {
if (loading) { if (loading) {
return ( return (
<SafeAreaView <SafeAreaView
style={[styles.container, { backgroundColor: colors.darkBackground }]} style={[styles.container, {
backgroundColor: currentTheme.colors.darkBackground
}]}
edges={['bottom']} edges={['bottom']}
> >
<StatusBar <StatusBar
@ -191,8 +196,10 @@ const MetadataScreen = () => {
barStyle="light-content" barStyle="light-content"
/> />
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} /> <ActivityIndicator size="large" color={currentTheme.colors.primary} />
<Text style={[styles.loadingText, { color: colors.lightGray }]}> <Text style={[styles.loadingText, {
color: currentTheme.colors.mediumEmphasis
}]}>
Loading content... Loading content...
</Text> </Text>
</View> </View>
@ -203,7 +210,9 @@ const MetadataScreen = () => {
if (metadataError || !metadata) { if (metadataError || !metadata) {
return ( return (
<SafeAreaView <SafeAreaView
style={[styles.container, { backgroundColor: colors.darkBackground }]} style={[styles.container, {
backgroundColor: currentTheme.colors.darkBackground
}]}
edges={['bottom']} edges={['bottom']}
> >
<StatusBar <StatusBar
@ -215,22 +224,24 @@ const MetadataScreen = () => {
<MaterialIcons <MaterialIcons
name="error-outline" name="error-outline"
size={64} 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'} {metadataError || 'Content not found'}
</Text> </Text>
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.retryButton, styles.retryButton,
{ backgroundColor: colors.primary } { backgroundColor: currentTheme.colors.primary }
]} ]}
onPress={loadMetadata} onPress={loadMetadata}
> >
<MaterialIcons <MaterialIcons
name="refresh" name="refresh"
size={20} size={20}
color={colors.white} color={currentTheme.colors.white}
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
/> />
<Text style={styles.retryButtonText}>Try Again</Text> <Text style={styles.retryButtonText}>Try Again</Text>
@ -238,11 +249,11 @@ const MetadataScreen = () => {
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.backButton, styles.backButton,
{ borderColor: colors.primary } { borderColor: currentTheme.colors.primary }
]} ]}
onPress={handleBack} onPress={handleBack}
> >
<Text style={[styles.backButtonText, { color: colors.primary }]}> <Text style={[styles.backButtonText, { color: currentTheme.colors.primary }]}>
Go Back Go Back
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
@ -253,7 +264,9 @@ const MetadataScreen = () => {
return ( return (
<SafeAreaView <SafeAreaView
style={[styles.container, { backgroundColor: colors.darkBackground }]} style={[containerAnimatedStyle, styles.container, {
backgroundColor: currentTheme.colors.darkBackground
}]}
edges={['bottom']} edges={['bottom']}
> >
<StatusBar <StatusBar
@ -386,7 +399,6 @@ const styles = StyleSheet.create({
loadingText: { loadingText: {
marginTop: 16, marginTop: 16,
fontSize: 16, fontSize: 16,
textAlign: 'center',
}, },
errorContainer: { errorContainer: {
flex: 1, flex: 1,
@ -395,11 +407,10 @@ const styles = StyleSheet.create({
padding: 32, padding: 32,
}, },
errorText: { errorText: {
fontSize: 16, fontSize: 18,
textAlign: 'center', textAlign: 'center',
marginTop: 16, marginTop: 16,
marginBottom: 24, marginBottom: 24,
lineHeight: 24,
}, },
retryButton: { retryButton: {
flexDirection: 'row', flexDirection: 'row',
@ -411,7 +422,6 @@ const styles = StyleSheet.create({
marginBottom: 16, marginBottom: 16,
}, },
retryButtonText: { retryButtonText: {
color: colors.white,
fontSize: 16, fontSize: 16,
fontWeight: '600', fontWeight: '600',
}, },