mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-10 12:01:55 +00:00
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:
parent
190c1a7371
commit
188c6e37f1
8 changed files with 310 additions and 234 deletions
|
|
@ -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)}
|
||||||
|
layout={Layout}
|
||||||
|
>
|
||||||
|
<View style={styles.sectionHeader}>
|
||||||
|
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>Cast</Text>
|
||||||
|
</View>
|
||||||
|
<FlatList
|
||||||
horizontal
|
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,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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]) => {
|
||||||
|
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
|
// Get a short display name for the rating source
|
||||||
const getSourceLabel = (src: string): string => {
|
const getSourceLabel = (src: string): string => {
|
||||||
switch(src) {
|
switch(src) {
|
||||||
case 'imdb': return 'IMDb';
|
case 'imdb': return 'IMDb';
|
||||||
case 'tmdb': return 'TMDB';
|
case 'tmdb': return 'TMDB';
|
||||||
case 'tomatoes': return 'RT';
|
case 'tomatoes': return 'RT';
|
||||||
case 'audience': return 'Aud';
|
case 'audience': return 'Aud';
|
||||||
case 'metacritic': return 'Meta';
|
case 'metacritic': return 'Meta';
|
||||||
case 'letterboxd': return 'LBXD';
|
case 'letterboxd': return 'LBXD';
|
||||||
case 'trakt': return 'Trakt';
|
case 'trakt': return 'Trakt';
|
||||||
default: return src;
|
default: return src;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={source} style={styles.ratingItem}>
|
<View key={source} style={styles.ratingItem}>
|
||||||
{config.isImage ? (
|
<View style={styles.ratingIconContainer}>
|
||||||
<Image
|
{config.isImage ? (
|
||||||
source={config.icon}
|
<Image
|
||||||
style={styles.ratingIcon}
|
source={config.icon as any}
|
||||||
resizeMode="contain"
|
style={styles.ratingIconImage}
|
||||||
/>
|
resizeMode="contain"
|
||||||
) : (
|
/>
|
||||||
<config.icon
|
) : (
|
||||||
width={16}
|
<View style={styles.svgContainer}>
|
||||||
height={16}
|
{React.createElement(config.icon as any, {
|
||||||
style={styles.ratingIcon}
|
width: 24,
|
||||||
/>
|
height: 24,
|
||||||
)}
|
})}
|
||||||
<Text style={[styles.ratingValue, {color: config.color}]}>
|
</View>
|
||||||
{displayValue}{config.suffix}
|
)}
|
||||||
</Text>
|
</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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -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',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue