NuvioStreaming/src/components/metadata/RatingsSection.tsx

314 lines
No EOL
8.6 KiB
TypeScript

import React, { useEffect, useState, useRef } from 'react';
import { View, Text, StyleSheet, ActivityIndicator, Image, Animated } from 'react-native';
import { colors } from '../../styles/colors';
import { useMDBListRatings } from '../../hooks/useMDBListRatings';
import { logger } from '../../utils/logger';
import { MaterialIcons } from '@expo/vector-icons';
import { FontAwesome } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { isMDBListEnabled, RATING_PROVIDERS_STORAGE_KEY } from '../../screens/MDBListSettingsScreen';
// Import SVG icons
import LetterboxdIcon from '../../../assets/rating-icons/letterboxd.svg';
import MetacriticIcon from '../../../assets/rating-icons/Metacritic.png';
import RottenTomatoesIcon from '../../../assets/rating-icons/RottenTomatoes.svg';
import TMDBIcon from '../../../assets/rating-icons/tmdb.svg';
import TraktIcon from '../../../assets/rating-icons/trakt.svg';
import AudienceScoreIcon from '../../../assets/rating-icons/audienscore.png';
export const RATING_PROVIDERS = {
imdb: {
name: 'IMDb',
color: '#F5C518',
},
tmdb: {
name: 'TMDB',
color: '#01B4E4',
},
trakt: {
name: 'Trakt',
color: '#ED1C24',
},
letterboxd: {
name: 'Letterboxd',
color: '#00E054',
},
tomatoes: {
name: 'Rotten Tomatoes',
color: '#FA320A',
},
audience: {
name: 'Audience Score',
color: '#FA320A',
},
metacritic: {
name: 'Metacritic',
color: '#FFCC33',
}
} as const;
interface RatingsSectionProps {
imdbId: string;
type: 'movie' | 'show';
}
export const RatingsSection: React.FC<RatingsSectionProps> = ({ imdbId, type }) => {
const { ratings, loading, error } = useMDBListRatings(imdbId, type);
const [enabledProviders, setEnabledProviders] = useState<Record<string, boolean>>({});
const [isMDBEnabled, setIsMDBEnabled] = useState(true);
const fadeAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
loadProviderSettings();
checkMDBListEnabled();
}, []);
const checkMDBListEnabled = async () => {
try {
const enabled = await isMDBListEnabled();
setIsMDBEnabled(enabled);
logger.log('[RatingsSection] MDBList enabled:', enabled);
} catch (error) {
logger.error('[RatingsSection] Failed to check if MDBList is enabled:', error);
setIsMDBEnabled(true); // Default to enabled
}
};
const loadProviderSettings = async () => {
try {
const savedSettings = await AsyncStorage.getItem(RATING_PROVIDERS_STORAGE_KEY);
if (savedSettings) {
setEnabledProviders(JSON.parse(savedSettings));
} else {
// Default all providers to enabled
const defaultSettings = Object.keys(RATING_PROVIDERS).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {} as Record<string, boolean>);
setEnabledProviders(defaultSettings);
}
} catch (error) {
logger.error('[RatingsSection] Failed to load provider settings:', error);
}
};
useEffect(() => {
logger.log(`[RatingsSection] Mounted for ${type}:`, imdbId);
return () => {
logger.log(`[RatingsSection] Unmounted for ${type}:`, imdbId);
};
}, [imdbId, type]);
useEffect(() => {
if (error) {
logger.error('[RatingsSection] Error state:', error);
}
}, [error]);
useEffect(() => {
if (ratings) {
logger.log('[RatingsSection] Received ratings:', ratings);
}
}, [ratings]);
useEffect(() => {
if (ratings && Object.keys(ratings).length > 0) {
// Start fade-in animation when ratings are loaded
Animated.timing(fadeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
}
}, [ratings, fadeAnim]);
// If MDBList is disabled, don't show anything
if (!isMDBEnabled) {
logger.log('[RatingsSection] MDBList is disabled, not showing ratings');
return null;
}
if (loading) {
logger.log('[RatingsSection] Loading state');
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="small" color={colors.primary} />
</View>
);
}
if (error || !ratings || Object.keys(ratings).length === 0) {
logger.log('[RatingsSection] No ratings to display');
return null;
}
logger.log('[RatingsSection] Rendering ratings:', Object.keys(ratings).length);
// Define the order and icons/colors for the ratings
const ratingConfig = {
imdb: {
icon: require('../../../assets/rating-icons/imdb.png'),
isImage: true,
color: '#F5C518',
prefix: '',
suffix: '',
transform: (value: number) => value.toFixed(1)
},
tmdb: {
icon: TMDBIcon,
isImage: false,
color: '#01B4E4',
prefix: '',
suffix: '',
transform: (value: number) => value.toFixed(0)
},
trakt: {
icon: TraktIcon,
isImage: false,
color: '#ED1C24',
prefix: '',
suffix: '',
transform: (value: number) => value.toFixed(0)
},
letterboxd: {
icon: LetterboxdIcon,
isImage: false,
color: '#00E054',
prefix: '',
suffix: '',
transform: (value: number) => value.toFixed(1)
},
tomatoes: {
icon: RottenTomatoesIcon,
isImage: false,
color: '#FA320A',
prefix: '',
suffix: '%',
transform: (value: number) => Math.round(value).toString()
},
audience: {
icon: AudienceScoreIcon,
isImage: true,
color: '#FA320A',
prefix: '',
suffix: '%',
transform: (value: number) => Math.round(value).toString()
},
metacritic: {
icon: MetacriticIcon,
isImage: true,
color: '#FFCC33',
prefix: '',
suffix: '',
transform: (value: number) => Math.round(value).toString()
}
};
// Priority: IMDB, TMDB, Tomatoes, Metacritic
const priorityOrder = ['imdb', 'tmdb', 'tomatoes', 'metacritic', 'trakt', 'letterboxd', 'audience'];
const displayRatings = priorityOrder
.filter(source =>
source in ratings &&
ratings[source as keyof typeof ratings] !== undefined &&
(enabledProviders[source] ?? true) // Show by default if setting not found
)
.map(source => [source, ratings[source as keyof typeof ratings]!]);
return (
<Animated.View
style={[
styles.container,
{
opacity: fadeAnim,
transform: [{
translateY: fadeAnim.interpolate({
inputRange: [0, 1],
outputRange: [10, 0],
}),
}],
},
]}
>
{displayRatings.map(([source, value]) => {
const config = ratingConfig[source as keyof typeof ratingConfig];
const numericValue = typeof value === 'string' ? parseFloat(value) : value;
const displayValue = config.transform(numericValue);
// Get a short display name for the rating source
const getSourceLabel = (src: string): string => {
switch(src) {
case 'imdb': return 'IMDb';
case 'tmdb': return 'TMDB';
case 'tomatoes': return 'RT';
case 'audience': return 'Aud';
case 'metacritic': return 'Meta';
case 'letterboxd': return 'LBXD';
case 'trakt': return 'Trakt';
default: return src;
}
};
return (
<View key={source} style={styles.ratingItem}>
{config.isImage ? (
<Image
source={config.icon}
style={styles.ratingIcon}
resizeMode="contain"
/>
) : (
<config.icon
width={16}
height={16}
style={styles.ratingIcon}
/>
)}
<Text style={[styles.ratingValue, {color: config.color}]}>
{displayValue}{config.suffix}
</Text>
</View>
);
})}
</Animated.View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginTop: 8,
marginBottom: 16,
paddingHorizontal: 12,
gap: 4,
},
loadingContainer: {
alignItems: 'center',
justifyContent: 'center',
height: 40,
marginVertical: 16,
},
ratingItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.4)',
paddingVertical: 3,
paddingHorizontal: 4,
borderRadius: 4,
},
ratingIcon: {
width: 16,
height: 16,
marginRight: 3,
alignSelf: 'center',
},
ratingValue: {
fontSize: 13,
fontWeight: 'bold',
},
ratingLabel: {
fontSize: 11,
opacity: 0.9,
},
});