This update introduces robust ID parsing and conversion logic in the HeroSection component, allowing for seamless navigation based on TMDB and IMDb IDs. Additionally, the useMetadataAssets hook has been optimized to manage logo and banner fetching more effectively, incorporating source tracking to prevent mixing assets from different sources. The changes improve error handling and logging for better debugging and user experience. The LogoSourceSettings component has also been updated to clarify the source selection process for logos and backgrounds.
805 lines
No EOL
25 KiB
TypeScript
805 lines
No EOL
25 KiB
TypeScript
import React, { useState, useEffect, useRef, useCallback, memo, Suspense } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
ActivityIndicator,
|
|
SafeAreaView,
|
|
TouchableOpacity,
|
|
Platform,
|
|
StatusBar,
|
|
} from 'react-native';
|
|
import { Image } from 'expo-image';
|
|
import { BlurView } from 'expo-blur';
|
|
import { colors } from '../styles';
|
|
import { TMDBService, TMDBShow as Show, TMDBSeason, TMDBEpisode } from '../services/tmdbService';
|
|
import { RouteProp } from '@react-navigation/native';
|
|
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
|
import axios from 'axios';
|
|
import Animated, { FadeIn, SlideInRight, withTiming, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
|
import { logger } from '../utils/logger';
|
|
|
|
type RootStackParamList = {
|
|
ShowRatings: { showId: number };
|
|
};
|
|
|
|
type ShowRatingsRouteProp = RouteProp<RootStackParamList, 'ShowRatings'>;
|
|
|
|
type RatingSource = 'tmdb' | 'imdb' | 'tvmaze';
|
|
|
|
interface TVMazeEpisode {
|
|
id: number;
|
|
rating: {
|
|
average: number | null;
|
|
};
|
|
season: number;
|
|
number: number;
|
|
}
|
|
|
|
interface TVMazeShow {
|
|
id: number;
|
|
externals: {
|
|
imdb: string | null;
|
|
thetvdb: number | null;
|
|
};
|
|
_embedded?: {
|
|
episodes: TVMazeEpisode[];
|
|
};
|
|
}
|
|
|
|
interface Props {
|
|
route: ShowRatingsRouteProp;
|
|
}
|
|
|
|
const getRatingColor = (rating: number): string => {
|
|
if (rating >= 9.0) return '#186A3B'; // Awesome
|
|
if (rating >= 8.5) return '#28B463'; // Great
|
|
if (rating >= 8.0) return '#28B463'; // Great
|
|
if (rating >= 7.5) return '#F4D03F'; // Good
|
|
if (rating >= 7.0) return '#F39C12'; // Regular
|
|
if (rating >= 6.0) return '#E74C3C'; // Bad
|
|
return '#633974'; // Garbage
|
|
};
|
|
|
|
// Memoized components
|
|
const RatingCell = memo(({ episode, ratingSource, getTVMazeRating, isCurrentSeason }: {
|
|
episode: TMDBEpisode;
|
|
ratingSource: RatingSource;
|
|
getTVMazeRating: (seasonNumber: number, episodeNumber: number) => number | null;
|
|
isCurrentSeason: (episode: TMDBEpisode) => boolean;
|
|
}) => {
|
|
const getRatingForSource = useCallback((episode: TMDBEpisode): number | null => {
|
|
switch (ratingSource) {
|
|
case 'imdb':
|
|
return episode.imdb_rating || null;
|
|
case 'tmdb':
|
|
return episode.vote_average || null;
|
|
case 'tvmaze':
|
|
return getTVMazeRating(episode.season_number, episode.episode_number);
|
|
default:
|
|
return null;
|
|
}
|
|
}, [ratingSource, getTVMazeRating]);
|
|
|
|
const isRatingPotentiallyInaccurate = useCallback((episode: TMDBEpisode): boolean => {
|
|
const rating = getRatingForSource(episode);
|
|
if (!rating) return false;
|
|
|
|
if (ratingSource === 'tmdb' && episode.imdb_rating) {
|
|
const difference = Math.abs(rating - episode.imdb_rating);
|
|
return difference >= 2;
|
|
}
|
|
|
|
return false;
|
|
}, [getRatingForSource, ratingSource]);
|
|
|
|
const rating = getRatingForSource(episode);
|
|
const isInaccurate = isRatingPotentiallyInaccurate(episode);
|
|
const isCurrent = isCurrentSeason(episode);
|
|
|
|
if (!rating) {
|
|
if (!episode.air_date || new Date(episode.air_date) > new Date()) {
|
|
return (
|
|
<View style={[styles.ratingCell, { backgroundColor: colors.darkGray }]}>
|
|
<MaterialIcons name="schedule" size={16} color={colors.lightGray} />
|
|
</View>
|
|
);
|
|
}
|
|
return (
|
|
<View style={[styles.ratingCell, { backgroundColor: colors.darkGray }]}>
|
|
<Text style={[styles.ratingText, { color: colors.lightGray }]}>—</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.ratingCellContainer}>
|
|
<View style={[
|
|
styles.ratingCell,
|
|
{
|
|
backgroundColor: getRatingColor(rating),
|
|
opacity: isCurrent ? 0.7 : 1
|
|
}
|
|
]}>
|
|
<Text style={styles.ratingText}>{rating.toFixed(1)}</Text>
|
|
</View>
|
|
{(isInaccurate || isCurrent) && (
|
|
<MaterialIcons
|
|
name={isCurrent ? "schedule" : "warning"}
|
|
size={12}
|
|
color={isCurrent ? colors.primary : colors.warning}
|
|
style={styles.warningIcon}
|
|
/>
|
|
)}
|
|
</View>
|
|
);
|
|
});
|
|
|
|
const RatingSourceToggle = memo(({ ratingSource, setRatingSource }: {
|
|
ratingSource: RatingSource;
|
|
setRatingSource: (source: RatingSource) => void;
|
|
}) => (
|
|
<View style={styles.ratingSourceContainer}>
|
|
<Text style={styles.ratingSourceTitle}>Rating Source:</Text>
|
|
<View style={styles.ratingSourceButtons}>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.sourceButton,
|
|
ratingSource === 'imdb' && styles.sourceButtonActive
|
|
]}
|
|
onPress={() => setRatingSource('imdb')}
|
|
>
|
|
<Text style={[
|
|
styles.sourceButtonText,
|
|
ratingSource === 'imdb' && styles.sourceButtonTextActive
|
|
]}>IMDb</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.sourceButton,
|
|
ratingSource === 'tmdb' && styles.sourceButtonActive
|
|
]}
|
|
onPress={() => setRatingSource('tmdb')}
|
|
>
|
|
<Text style={[
|
|
styles.sourceButtonText,
|
|
ratingSource === 'tmdb' && styles.sourceButtonTextActive
|
|
]}>TMDB</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.sourceButton,
|
|
ratingSource === 'tvmaze' && styles.sourceButtonActive
|
|
]}
|
|
onPress={() => setRatingSource('tvmaze')}
|
|
>
|
|
<Text style={[
|
|
styles.sourceButtonText,
|
|
ratingSource === 'tvmaze' && styles.sourceButtonTextActive
|
|
]}>TVMaze</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
));
|
|
|
|
const ShowInfo = memo(({ show }: { show: Show | null }) => (
|
|
<View style={styles.showInfo}>
|
|
<Image
|
|
source={{ uri: `https://image.tmdb.org/t/p/w500${show?.poster_path}` }}
|
|
style={styles.poster}
|
|
contentFit="cover"
|
|
/>
|
|
<View style={styles.showDetails}>
|
|
<Text style={styles.showTitle}>{show?.name}</Text>
|
|
<Text style={styles.showYear}>
|
|
{show?.first_air_date ? `${new Date(show.first_air_date).getFullYear()} - ${show.last_air_date ? new Date(show.last_air_date).getFullYear() : 'Present'}` : ''}
|
|
</Text>
|
|
<View style={styles.episodeCountContainer}>
|
|
<MaterialIcons name="tv" size={16} color={colors.primary} />
|
|
<Text style={styles.episodeCount}>
|
|
{show?.number_of_seasons} Seasons • {show?.number_of_episodes} Episodes
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
));
|
|
|
|
const ShowRatingsScreen = ({ route }: Props) => {
|
|
const { showId } = route.params;
|
|
const [show, setShow] = useState<Show | null>(null);
|
|
const [seasons, setSeasons] = useState<TMDBSeason[]>([]);
|
|
const [tvmazeEpisodes, setTvmazeEpisodes] = useState<TVMazeEpisode[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [loadingSeasons, setLoadingSeasons] = useState(false);
|
|
const [loadedSeasons, setLoadedSeasons] = useState<number[]>([]);
|
|
const [ratingSource, setRatingSource] = useState<RatingSource>('tmdb');
|
|
const [visibleSeasonRange, setVisibleSeasonRange] = useState({ start: 0, end: 8 });
|
|
const [loadingProgress, setLoadingProgress] = useState(0);
|
|
const ratingsCache = useRef<{[key: string]: number | null}>({});
|
|
|
|
const fetchTVMazeData = async (imdbId: string) => {
|
|
try {
|
|
const lookupResponse = await axios.get(`https://api.tvmaze.com/lookup/shows?imdb=${imdbId}`);
|
|
const tvmazeId = lookupResponse.data?.id;
|
|
|
|
if (tvmazeId) {
|
|
const showResponse = await axios.get(`https://api.tvmaze.com/shows/${tvmazeId}?embed=episodes`);
|
|
if (showResponse.data?._embedded?.episodes) {
|
|
setTvmazeEpisodes(showResponse.data._embedded.episodes);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error('Error fetching TVMaze data:', error);
|
|
}
|
|
};
|
|
|
|
const loadMoreSeasons = async () => {
|
|
if (!show || loadingSeasons) return;
|
|
|
|
setLoadingSeasons(true);
|
|
try {
|
|
const tmdb = TMDBService.getInstance();
|
|
const seasonsToLoad = show.seasons
|
|
.filter(season =>
|
|
season.season_number > 0 &&
|
|
!loadedSeasons.includes(season.season_number) &&
|
|
season.season_number > visibleSeasonRange.start &&
|
|
season.season_number <= visibleSeasonRange.end
|
|
);
|
|
|
|
// Load seasons in parallel in larger batches
|
|
const batchSize = 4; // Load 4 seasons at a time
|
|
const batches = [];
|
|
|
|
for (let i = 0; i < seasonsToLoad.length; i += batchSize) {
|
|
const batch = seasonsToLoad.slice(i, i + batchSize);
|
|
batches.push(batch);
|
|
}
|
|
|
|
let loadedCount = 0;
|
|
const totalToLoad = seasonsToLoad.length;
|
|
|
|
for (const batch of batches) {
|
|
const batchResults = await Promise.all(
|
|
batch.map(season =>
|
|
tmdb.getSeasonDetails(showId, season.season_number, show.name)
|
|
)
|
|
);
|
|
|
|
const validResults = batchResults.filter((s): s is TMDBSeason => s !== null);
|
|
setSeasons(prev => [...prev, ...validResults]);
|
|
setLoadedSeasons(prev => [...prev, ...batch.map(s => s.season_number)]);
|
|
|
|
loadedCount += batch.length;
|
|
setLoadingProgress((loadedCount / totalToLoad) * 100);
|
|
}
|
|
} catch (error) {
|
|
logger.error('Error loading more seasons:', error);
|
|
} finally {
|
|
setLoadingProgress(0);
|
|
setLoadingSeasons(false);
|
|
}
|
|
};
|
|
|
|
const onScroll = useCallback((event: any) => {
|
|
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
|
|
const isCloseToRight = (contentOffset.x + layoutMeasurement.width) >= (contentSize.width * 0.8);
|
|
|
|
if (isCloseToRight && show && !loadingSeasons) {
|
|
const maxSeasons = Math.max(...show.seasons.map(s => s.season_number));
|
|
if (visibleSeasonRange.end < maxSeasons) {
|
|
setVisibleSeasonRange(prev => ({
|
|
start: prev.end,
|
|
end: Math.min(prev.end + 8, maxSeasons)
|
|
}));
|
|
}
|
|
}
|
|
}, [show, loadingSeasons, visibleSeasonRange.end]);
|
|
|
|
useEffect(() => {
|
|
const fetchShowData = async () => {
|
|
try {
|
|
const tmdb = TMDBService.getInstance();
|
|
|
|
// Log the showId being used
|
|
logger.log(`[ShowRatingsScreen] Fetching show details for ID: ${showId}`);
|
|
|
|
const showData = await tmdb.getTVShowDetails(showId);
|
|
if (showData) {
|
|
setShow(showData);
|
|
|
|
// Get external IDs to fetch TVMaze data
|
|
const externalIds = await tmdb.getShowExternalIds(showId);
|
|
if (externalIds?.imdb_id) {
|
|
fetchTVMazeData(externalIds.imdb_id);
|
|
}
|
|
|
|
// Set initial season range
|
|
const initialEnd = Math.min(8, Math.max(...showData.seasons.map(s => s.season_number)));
|
|
setVisibleSeasonRange({ start: 0, end: initialEnd });
|
|
}
|
|
} catch (error) {
|
|
logger.error('Error fetching show data:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchShowData();
|
|
}, [showId]);
|
|
|
|
useEffect(() => {
|
|
loadMoreSeasons();
|
|
}, [visibleSeasonRange]);
|
|
|
|
const getTVMazeRating = useCallback((seasonNumber: number, episodeNumber: number): number | null => {
|
|
const episode = tvmazeEpisodes.find(
|
|
ep => ep.season === seasonNumber && ep.number === episodeNumber
|
|
);
|
|
return episode?.rating?.average || null;
|
|
}, [tvmazeEpisodes]);
|
|
|
|
const isCurrentSeason = useCallback((episode: TMDBEpisode): boolean => {
|
|
if (!seasons.length || !episode.air_date) return false;
|
|
|
|
const latestSeasonNumber = Math.max(...seasons.map(s => s.season_number));
|
|
if (episode.season_number !== latestSeasonNumber) return false;
|
|
|
|
const now = new Date();
|
|
const airDate = new Date(episode.air_date);
|
|
const monthsDiff = (now.getFullYear() - airDate.getFullYear()) * 12 +
|
|
(now.getMonth() - airDate.getMonth());
|
|
|
|
return monthsDiff <= 6;
|
|
}, [seasons]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<View style={[styles.container, { backgroundColor: colors.black }]}>
|
|
<StatusBar
|
|
translucent
|
|
backgroundColor="transparent"
|
|
barStyle="light-content"
|
|
/>
|
|
<SafeAreaView style={{ flex: 1 }}>
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color={colors.primary} />
|
|
</View>
|
|
</SafeAreaView>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={[styles.container, { backgroundColor: Platform.OS === 'ios' ? 'transparent' : colors.black }]}>
|
|
{Platform.OS === 'ios' && (
|
|
<BlurView
|
|
style={StyleSheet.absoluteFill}
|
|
tint="dark"
|
|
intensity={60}
|
|
/>
|
|
)}
|
|
<StatusBar
|
|
translucent
|
|
backgroundColor="transparent"
|
|
barStyle="light-content"
|
|
/>
|
|
<SafeAreaView style={{ flex: 1 }}>
|
|
<Suspense fallback={
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color={colors.primary} />
|
|
</View>
|
|
}>
|
|
<ScrollView
|
|
style={styles.scrollView}
|
|
showsVerticalScrollIndicator={false}
|
|
removeClippedSubviews={true}
|
|
>
|
|
<View style={styles.content}>
|
|
<Animated.View
|
|
entering={FadeIn.duration(150)}
|
|
style={styles.showInfoContainer}
|
|
>
|
|
<ShowInfo show={show} />
|
|
</Animated.View>
|
|
|
|
<Animated.View
|
|
entering={FadeIn.delay(50).duration(150)}
|
|
style={styles.ratingSourceContainer}
|
|
>
|
|
<RatingSourceToggle ratingSource={ratingSource} setRatingSource={setRatingSource} />
|
|
</Animated.View>
|
|
|
|
<Animated.View
|
|
entering={FadeIn.delay(100).duration(150)}
|
|
style={styles.legend}
|
|
>
|
|
{/* Legend */}
|
|
<View style={styles.legend}>
|
|
<Text style={styles.legendTitle}>Rating Scale</Text>
|
|
<View style={styles.legendItems}>
|
|
<View style={styles.legendItem}>
|
|
<View style={[styles.legendColor, { backgroundColor: '#186A3B' }]} />
|
|
<Text style={styles.legendText}>Awesome (9.0+)</Text>
|
|
</View>
|
|
<View style={styles.legendItem}>
|
|
<View style={[styles.legendColor, { backgroundColor: '#28B463' }]} />
|
|
<Text style={styles.legendText}>Great (8.0-8.9)</Text>
|
|
</View>
|
|
<View style={styles.legendItem}>
|
|
<View style={[styles.legendColor, { backgroundColor: '#F4D03F' }]} />
|
|
<Text style={styles.legendText}>Good (7.5-7.9)</Text>
|
|
</View>
|
|
<View style={styles.legendItem}>
|
|
<View style={[styles.legendColor, { backgroundColor: '#F39C12' }]} />
|
|
<Text style={styles.legendText}>Regular (7.0-7.4)</Text>
|
|
</View>
|
|
<View style={styles.legendItem}>
|
|
<View style={[styles.legendColor, { backgroundColor: '#E74C3C' }]} />
|
|
<Text style={styles.legendText}>Bad (6.0-6.9)</Text>
|
|
</View>
|
|
<View style={styles.legendItem}>
|
|
<View style={[styles.legendColor, { backgroundColor: '#633974' }]} />
|
|
<Text style={styles.legendText}>Garbage ({'<'}6.0)</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.warningLegends}>
|
|
<View style={styles.warningLegend}>
|
|
<MaterialIcons name="warning" size={16} color={colors.warning} />
|
|
<Text style={styles.warningText}>Rating differs significantly from IMDb</Text>
|
|
</View>
|
|
<View style={styles.warningLegend}>
|
|
<MaterialIcons name="schedule" size={16} color={colors.primary} />
|
|
<Text style={styles.warningText}>Current season (ratings may change)</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Animated.View>
|
|
|
|
<Animated.View
|
|
entering={FadeIn.delay(150).duration(150)}
|
|
style={styles.ratingsGrid}
|
|
>
|
|
{/* Ratings Grid */}
|
|
<View style={styles.ratingsGrid}>
|
|
<View style={styles.gridContainer}>
|
|
{/* Fixed Episode Column */}
|
|
<View style={styles.fixedColumn}>
|
|
<View style={styles.episodeColumn}>
|
|
<Text style={styles.headerText}>Episode</Text>
|
|
</View>
|
|
{Array.from({ length: Math.max(...seasons.map(s => s.episodes.length)) }).map((_, episodeIndex) => (
|
|
<View key={`e${episodeIndex + 1}`} style={styles.episodeCell}>
|
|
<Text style={styles.episodeText}>E{episodeIndex + 1}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
|
|
{/* Scrollable Seasons */}
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
style={styles.seasonsScrollView}
|
|
onScroll={onScroll}
|
|
scrollEventThrottle={16}
|
|
>
|
|
<View>
|
|
{/* Seasons Header */}
|
|
<View style={styles.gridHeader}>
|
|
{seasons.map((season) => (
|
|
<View key={`s${season.season_number}`} style={styles.ratingColumn}>
|
|
<Text style={styles.headerText}>S{season.season_number}</Text>
|
|
</View>
|
|
))}
|
|
{loadingSeasons && (
|
|
<View style={[styles.ratingColumn, styles.loadingColumn]}>
|
|
<View style={styles.loadingProgressContainer}>
|
|
<ActivityIndicator size="small" color={colors.primary} />
|
|
{loadingProgress > 0 && (
|
|
<Text style={styles.loadingProgressText}>
|
|
{Math.round(loadingProgress)}%
|
|
</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* Episodes Grid */}
|
|
{Array.from({ length: Math.max(...seasons.map(s => s.episodes.length)) }).map((_, episodeIndex) => (
|
|
<View key={`e${episodeIndex + 1}`} style={styles.gridRow}>
|
|
{seasons.map((season) => (
|
|
<View key={`s${season.season_number}e${episodeIndex + 1}`} style={styles.ratingColumn}>
|
|
{season.episodes[episodeIndex] &&
|
|
<RatingCell
|
|
episode={season.episodes[episodeIndex]}
|
|
ratingSource={ratingSource}
|
|
getTVMazeRating={getTVMazeRating}
|
|
isCurrentSeason={isCurrentSeason}
|
|
/>
|
|
}
|
|
</View>
|
|
))}
|
|
{loadingSeasons && <View style={[styles.ratingColumn, styles.loadingColumn]} />}
|
|
</View>
|
|
))}
|
|
</View>
|
|
</ScrollView>
|
|
</View>
|
|
</View>
|
|
</Animated.View>
|
|
</View>
|
|
</ScrollView>
|
|
</Suspense>
|
|
</SafeAreaView>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
},
|
|
scrollView: {
|
|
flex: 1,
|
|
},
|
|
content: {
|
|
padding: 8,
|
|
paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8,
|
|
},
|
|
loadingContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
showInfoContainer: {
|
|
marginBottom: 12,
|
|
},
|
|
showInfo: {
|
|
flexDirection: 'row',
|
|
marginBottom: 12,
|
|
backgroundColor: Platform.OS === 'ios' ? 'transparent' : colors.darkBackground,
|
|
borderRadius: 8,
|
|
padding: 8,
|
|
},
|
|
poster: {
|
|
width: 80,
|
|
height: 120,
|
|
borderRadius: 6,
|
|
},
|
|
showDetails: {
|
|
flex: 1,
|
|
marginLeft: 8,
|
|
justifyContent: 'center',
|
|
},
|
|
showTitle: {
|
|
fontSize: 18,
|
|
fontWeight: '800',
|
|
color: colors.white,
|
|
marginBottom: 2,
|
|
letterSpacing: 0.5,
|
|
},
|
|
showYear: {
|
|
fontSize: 13,
|
|
color: colors.lightGray,
|
|
marginBottom: 6,
|
|
},
|
|
episodeCountContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 4,
|
|
},
|
|
episodeCount: {
|
|
fontSize: 12,
|
|
color: colors.lightGray,
|
|
},
|
|
ratingSection: {
|
|
backgroundColor: colors.darkBackground,
|
|
borderRadius: 8,
|
|
padding: 8,
|
|
marginBottom: 12,
|
|
},
|
|
ratingSourceContainer: {
|
|
marginBottom: 8,
|
|
},
|
|
ratingSourceTitle: {
|
|
fontSize: 14,
|
|
fontWeight: '700',
|
|
color: colors.white,
|
|
marginBottom: 6,
|
|
letterSpacing: 0.5,
|
|
},
|
|
ratingSourceButtons: {
|
|
flexDirection: 'row',
|
|
gap: 8,
|
|
},
|
|
sourceButton: {
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 6,
|
|
borderRadius: 6,
|
|
borderWidth: 1,
|
|
borderColor: colors.lightGray,
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
},
|
|
sourceButtonActive: {
|
|
backgroundColor: colors.primary,
|
|
borderColor: colors.primary,
|
|
},
|
|
sourceButtonText: {
|
|
color: colors.lightGray,
|
|
fontSize: 14,
|
|
fontWeight: '600',
|
|
},
|
|
sourceButtonTextActive: {
|
|
color: colors.white,
|
|
fontWeight: '700',
|
|
},
|
|
tmdbDisclaimer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: colors.black + '40',
|
|
padding: 6,
|
|
borderRadius: 6,
|
|
marginTop: 8,
|
|
gap: 6,
|
|
},
|
|
tmdbDisclaimerText: {
|
|
color: colors.lightGray,
|
|
fontSize: 12,
|
|
flex: 1,
|
|
lineHeight: 16,
|
|
},
|
|
legend: {
|
|
backgroundColor: Platform.OS === 'ios' ? 'transparent' : colors.darkBackground,
|
|
borderRadius: 8,
|
|
padding: 8,
|
|
marginBottom: 12,
|
|
},
|
|
legendTitle: {
|
|
fontSize: 14,
|
|
fontWeight: '700',
|
|
color: colors.white,
|
|
marginBottom: 8,
|
|
letterSpacing: 0.5,
|
|
},
|
|
legendItems: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
gap: 8,
|
|
marginBottom: 12,
|
|
},
|
|
legendItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
minWidth: '45%',
|
|
marginBottom: 2,
|
|
},
|
|
legendColor: {
|
|
width: 14,
|
|
height: 14,
|
|
borderRadius: 3,
|
|
marginRight: 6,
|
|
},
|
|
legendText: {
|
|
color: colors.lightGray,
|
|
fontSize: 12,
|
|
},
|
|
warningLegends: {
|
|
marginTop: 8,
|
|
gap: 6,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.black + '40',
|
|
paddingTop: 8,
|
|
},
|
|
warningLegend: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
},
|
|
warningText: {
|
|
color: colors.lightGray,
|
|
fontSize: 11,
|
|
flex: 1,
|
|
},
|
|
ratingsGrid: {
|
|
backgroundColor: Platform.OS === 'ios' ? 'transparent' : colors.darkBackground,
|
|
borderRadius: 8,
|
|
padding: 8,
|
|
},
|
|
gridContainer: {
|
|
flexDirection: 'row',
|
|
},
|
|
fixedColumn: {
|
|
width: 40,
|
|
borderRightWidth: 1,
|
|
borderRightColor: colors.black + '40',
|
|
},
|
|
seasonsScrollView: {
|
|
flex: 1,
|
|
},
|
|
gridHeader: {
|
|
flexDirection: 'row',
|
|
marginBottom: 8,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: colors.black + '40',
|
|
paddingBottom: 6,
|
|
paddingLeft: 6,
|
|
},
|
|
gridRow: {
|
|
flexDirection: 'row',
|
|
marginBottom: 6,
|
|
paddingLeft: 6,
|
|
},
|
|
episodeCell: {
|
|
height: 28,
|
|
justifyContent: 'center',
|
|
paddingRight: 6,
|
|
},
|
|
episodeColumn: {
|
|
height: 28,
|
|
justifyContent: 'center',
|
|
marginBottom: 8,
|
|
paddingRight: 6,
|
|
},
|
|
ratingColumn: {
|
|
width: 40,
|
|
alignItems: 'center',
|
|
},
|
|
headerText: {
|
|
color: colors.white,
|
|
fontWeight: '700',
|
|
fontSize: 12,
|
|
letterSpacing: 0.5,
|
|
},
|
|
episodeText: {
|
|
color: colors.lightGray,
|
|
fontSize: 12,
|
|
fontWeight: '500',
|
|
},
|
|
ratingCell: {
|
|
width: 32,
|
|
height: 24,
|
|
borderRadius: 3,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
ratingText: {
|
|
color: colors.white,
|
|
fontSize: 12,
|
|
fontWeight: '700',
|
|
},
|
|
ratingCellContainer: {
|
|
position: 'relative',
|
|
width: 32,
|
|
height: 24,
|
|
},
|
|
warningIcon: {
|
|
position: 'absolute',
|
|
top: -4,
|
|
right: -4,
|
|
backgroundColor: colors.black,
|
|
borderRadius: 8,
|
|
padding: 1,
|
|
},
|
|
loadingColumn: {
|
|
width: 40,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
opacity: 0.7,
|
|
},
|
|
loadingProgressContainer: {
|
|
alignItems: 'center',
|
|
gap: 4,
|
|
},
|
|
loadingProgressText: {
|
|
color: colors.primary,
|
|
fontSize: 10,
|
|
fontWeight: '600',
|
|
},
|
|
});
|
|
|
|
export default memo(ShowRatingsScreen, (prevProps, nextProps) => {
|
|
return prevProps.route.params.showId === nextProps.route.params.showId;
|
|
});
|