removed reanimated

This commit is contained in:
tapframe 2025-08-02 14:26:47 +05:30
parent 604b38ba20
commit 332cf99f67
14 changed files with 1100 additions and 2262 deletions

View file

@ -19,7 +19,7 @@ import AppNavigator, {
CustomNavigationDarkTheme,
CustomDarkTheme
} from './src/navigation/AppNavigator';
import 'react-native-reanimated';
// Removed react-native-reanimated import
import { CatalogProvider } from './src/contexts/CatalogContext';
import { GenreProvider } from './src/contexts/GenreContext';
import { TraktProvider } from './src/contexts/TraktContext';

View file

@ -49,7 +49,7 @@
"react-native-gesture-handler": "~2.20.2",
"react-native-immersive-mode": "^2.0.2",
"react-native-paper": "^5.13.1",
"react-native-reanimated": "~3.6.0",
"react-native-reanimated": "@latest",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-svg": "^15.11.2",

View file

@ -11,7 +11,7 @@ import {
Alert,
ActivityIndicator
} from 'react-native';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
// Removed react-native-reanimated import
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../../navigation/AppNavigator';
@ -572,7 +572,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
}
return (
<Animated.View entering={FadeIn.duration(300).delay(150)} style={styles.container}>
<View style={styles.container}>
<View style={styles.header}>
<View style={styles.titleContainer}>
<Text style={[styles.title, { color: currentTheme.colors.text }]}>Continue Watching</Text>
@ -609,13 +609,9 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
{/* Delete Indicator Overlay */}
{deletingItemId === item.id && (
<Animated.View
entering={FadeIn.duration(200)}
exiting={FadeOut.duration(200)}
style={styles.deletingOverlay}
>
<View style={styles.deletingOverlay}>
<ActivityIndicator size="large" color="#FFFFFF" />
</Animated.View>
</View>
)}
</View>
@ -700,7 +696,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
snapToAlignment="start"
ItemSeparatorComponent={() => <View style={{ width: 16 }} />}
/>
</Animated.View>
</View>
);
});

View file

@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React from 'react';
import {
View,
Text,
@ -13,19 +13,6 @@ import {
import { MaterialIcons } from '@expo/vector-icons';
import { Image as ExpoImage } from 'expo-image';
import { colors } from '../../styles/colors';
import Animated, {
useAnimatedStyle,
withTiming,
useSharedValue,
interpolate,
Extrapolate,
runOnJS,
} from 'react-native-reanimated';
import {
Gesture,
GestureDetector,
GestureHandlerRootView,
} from 'react-native-gesture-handler';
import { StreamingContent } from '../../services/catalogService';
interface DropUpMenuProps {
@ -36,144 +23,92 @@ interface DropUpMenuProps {
}
export const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) => {
const translateY = useSharedValue(300);
const opacity = useSharedValue(0);
const isDarkMode = useColorScheme() === 'dark';
const SNAP_THRESHOLD = 100;
useEffect(() => {
if (visible) {
opacity.value = withTiming(1, { duration: 200 });
translateY.value = withTiming(0, { duration: 300 });
} else {
opacity.value = withTiming(0, { duration: 200 });
translateY.value = withTiming(300, { duration: 300 });
}
}, [visible]);
const gesture = Gesture.Pan()
.onStart(() => {
// Store initial position if needed
})
.onUpdate((event) => {
if (event.translationY > 0) { // Only allow dragging downwards
translateY.value = event.translationY;
opacity.value = interpolate(
event.translationY,
[0, 300],
[1, 0],
Extrapolate.CLAMP
);
}
})
.onEnd((event) => {
if (event.translationY > SNAP_THRESHOLD || event.velocityY > 500) {
translateY.value = withTiming(300, { duration: 300 });
opacity.value = withTiming(0, { duration: 200 });
runOnJS(onClose)();
} else {
translateY.value = withTiming(0, { duration: 300 });
opacity.value = withTiming(1, { duration: 200 });
}
});
const overlayStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
}));
const menuStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
}));
const menuOptions = [
{
icon: item.inLibrary ? 'bookmark' : 'bookmark-border',
label: item.inLibrary ? 'Remove from Library' : 'Add to Library',
action: 'library'
},
{
icon: 'check-circle',
label: 'Mark as Watched',
action: 'watched'
},
{
icon: 'playlist-add',
label: 'Add to Playlist',
action: 'playlist'
},
{
icon: 'share',
label: 'Share',
action: 'share'
}
{ id: 'play', label: 'Play', icon: 'play-arrow' },
{ id: 'info', label: 'More Info', icon: 'info-outline' },
{ id: 'save', label: 'Add to My List', icon: 'bookmark-border' },
{ id: 'share', label: 'Share', icon: 'share' },
];
const backgroundColor = isDarkMode ? '#1A1A1A' : '#FFFFFF';
const handleOptionPress = (optionId: string) => {
onOptionSelect(optionId);
onClose();
};
return (
<Modal
visible={visible}
transparent
animationType="none"
animationType="slide"
onRequestClose={onClose}
>
<GestureHandlerRootView style={{ flex: 1 }}>
<Animated.View style={[styles.modalOverlay, overlayStyle]}>
<Pressable style={styles.modalOverlayPressable} onPress={onClose} />
<GestureDetector gesture={gesture}>
<Animated.View style={[styles.menuContainer, menuStyle, { backgroundColor }]}>
<View style={styles.dragHandle} />
<View style={styles.menuHeader}>
<ExpoImage
source={{ uri: item.poster }}
style={styles.menuPoster}
contentFit="cover"
<View style={styles.modalOverlay}>
<Pressable style={styles.modalOverlayPressable} onPress={onClose} />
<View style={[
styles.menuContainer,
{ backgroundColor: isDarkMode ? colors.darkBackground : colors.lightBackground }
]}>
{/* Drag Handle */}
<View style={styles.dragHandle} />
{/* Header with item info */}
<View style={styles.menuHeader}>
<ExpoImage
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
style={styles.menuPoster}
contentFit="cover"
cachePolicy="memory"
/>
<View style={styles.menuTitleContainer}>
<Text style={[
styles.menuTitle,
{ color: isDarkMode ? colors.white : colors.black }
]} numberOfLines={2}>
{item.name}
</Text>
{item.year && (
<Text style={[
styles.menuYear,
{ color: isDarkMode ? colors.textMuted : colors.textMutedDark }
]}>
{item.year}
</Text>
)}
</View>
</View>
{/* Menu Options */}
<View style={styles.menuOptions}>
{menuOptions.map((option, index) => (
<TouchableOpacity
key={option.id}
style={[
styles.menuOption,
{ borderBottomColor: isDarkMode ? colors.border : colors.border },
index === menuOptions.length - 1 && styles.lastMenuOption
]}
onPress={() => handleOptionPress(option.id)}
activeOpacity={0.7}
>
<MaterialIcons
name={option.icon as any}
size={24}
color={isDarkMode ? colors.white : colors.black}
/>
<View style={styles.menuTitleContainer}>
<Text style={[styles.menuTitle, { color: isDarkMode ? '#FFFFFF' : '#000000' }]}>
{item.name}
</Text>
{item.year && (
<Text style={[styles.menuYear, { color: isDarkMode ? '#999999' : '#666666' }]}>
{item.year}
</Text>
)}
</View>
</View>
<View style={styles.menuOptions}>
{menuOptions.map((option, index) => (
<TouchableOpacity
key={option.action}
style={[
styles.menuOption,
{ borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)' },
index === menuOptions.length - 1 && styles.lastMenuOption
]}
onPress={() => {
onOptionSelect(option.action);
onClose();
}}
>
<MaterialIcons
name={option.icon as "bookmark" | "check-circle" | "playlist-add" | "share" | "bookmark-border"}
size={24}
color={colors.primary}
/>
<Text style={[
styles.menuOptionText,
{ color: isDarkMode ? '#FFFFFF' : '#000000' }
]}>
{option.label}
</Text>
</TouchableOpacity>
))}
</View>
</Animated.View>
</GestureDetector>
</Animated.View>
</GestureHandlerRootView>
<Text style={[
styles.menuOptionText,
{ color: isDarkMode ? colors.white : colors.black }
]}>
{option.label}
</Text>
</TouchableOpacity>
))}
</View>
</View>
</View>
</Modal>
);
};
@ -254,4 +189,4 @@ const styles = StyleSheet.create({
},
});
export default DropUpMenu;
export default DropUpMenu;

View file

@ -17,14 +17,6 @@ import { RootStackParamList } from '../../navigation/AppNavigator';
import { LinearGradient } from 'expo-linear-gradient';
import { Image as ExpoImage } from 'expo-image';
import { MaterialIcons } from '@expo/vector-icons';
import Animated, {
FadeIn,
useAnimatedStyle,
useSharedValue,
withTiming,
Easing,
withDelay
} from 'react-native-reanimated';
import { StreamingContent } from '../../services/catalogService';
import { SkeletonFeatured } from './SkeletonLoaders';
import { isValidMetahubLogo, hasValidLogoFormat, isMetahubUrl, isTmdbUrl } from '../../utils/logoUtils';
@ -49,70 +41,18 @@ const NoFeaturedContent = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const styles = StyleSheet.create({
noContentContainer: {
height: height * 0.55,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 40,
backgroundColor: currentTheme.colors.elevation1,
borderRadius: 12,
marginBottom: 12,
},
noContentTitle: {
fontSize: 22,
fontWeight: 'bold',
color: currentTheme.colors.highEmphasis,
marginTop: 16,
marginBottom: 8,
textAlign: 'center',
},
noContentText: {
fontSize: 16,
color: currentTheme.colors.mediumEmphasis,
textAlign: 'center',
marginBottom: 24,
},
noContentButtons: {
flexDirection: 'row',
justifyContent: 'center',
gap: 16,
width: '100%',
},
noContentButton: {
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 30,
backgroundColor: currentTheme.colors.elevation3,
alignItems: 'center',
justifyContent: 'center'
},
noContentButtonText: {
color: currentTheme.colors.highEmphasis,
fontWeight: '600',
fontSize: 14,
}
});
return (
<View style={styles.noContentContainer}>
<MaterialIcons name="theaters" size={48} color={currentTheme.colors.mediumEmphasis} />
<Text style={styles.noContentTitle}>No Featured Content</Text>
<Text style={styles.noContentText}>
Install addons with catalogs or change the content source in your settings.
</Text>
<View style={styles.noContentButtons}>
<View style={[styles.featuredContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
<View style={styles.backgroundFallback}>
<MaterialIcons name="movie" size={64} color={currentTheme.colors.mediumEmphasis} />
<Text style={[styles.noContentText, { color: currentTheme.colors.mediumEmphasis }]}>
No featured content available
</Text>
<TouchableOpacity
style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={() => navigation.navigate('Addons')}
style={[styles.exploreButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={() => navigation.navigate('Search')}
>
<Text style={[styles.noContentButtonText, { color: currentTheme.colors.white }]}>Install Addons</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.noContentButton}
onPress={() => navigation.navigate('HomeScreenSettings')}
>
<Text style={styles.noContentButtonText}>Settings</Text>
<Text style={styles.exploreButtonText}>Explore Content</Text>
</TouchableOpacity>
</View>
</View>
@ -122,456 +62,193 @@ const NoFeaturedContent = () => {
const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: FeaturedContentProps) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const [bannerUrl, setBannerUrl] = useState<string | null>(null);
const [logoUrl, setLogoUrl] = useState<string | null>(null);
const [logoLoaded, setLogoLoaded] = useState(false);
const [bannerLoaded, setBannerLoaded] = useState(false);
const [showSkeleton, setShowSkeleton] = useState(true);
const [logoError, setLogoError] = useState(false);
const [bannerError, setBannerError] = useState(false);
const { settings } = useSettings();
const logoOpacity = useSharedValue(0);
const bannerOpacity = useSharedValue(0);
const posterOpacity = useSharedValue(0);
const prevContentIdRef = useRef<string | null>(null);
// Add state for tracking logo load errors
const [logoLoadError, setLogoLoadError] = useState(false);
// Add a ref to track logo fetch in progress
const logoFetchInProgress = useRef<boolean>(false);
const [logoUrl, setLogoUrl] = useState<string | null>(null);
const [isLogoLoading, setIsLogoLoading] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
const [imageError, setImageError] = useState(false);
// Removed TMDB service integration
// Enhanced poster transition animations
const posterScale = useSharedValue(1);
const posterTranslateY = useSharedValue(0);
const overlayOpacity = useSharedValue(0.15);
// Animation values
const posterAnimatedStyle = useAnimatedStyle(() => ({
opacity: posterOpacity.value,
transform: [
{ scale: posterScale.value },
{ translateY: posterTranslateY.value }
],
}));
const logoAnimatedStyle = useAnimatedStyle(() => ({
opacity: logoOpacity.value,
}));
const contentOpacity = useSharedValue(1); // Start visible
const buttonsOpacity = useSharedValue(1);
const contentAnimatedStyle = useAnimatedStyle(() => ({
opacity: contentOpacity.value,
}));
const buttonsAnimatedStyle = useAnimatedStyle(() => ({
opacity: buttonsOpacity.value,
}));
const overlayAnimatedStyle = useAnimatedStyle(() => ({
opacity: overlayOpacity.value,
}));
// Preload the image
const preloadImage = async (url: string): Promise<boolean> => {
// Skip if already cached to prevent redundant prefetch
if (imageCache[url]) return true;
try {
// Simplified validation to reduce CPU overhead
if (!url || typeof url !== 'string') return false;
// Use our optimized cache service instead of direct prefetch
await imageCacheService.getCachedImageUrl(url);
imageCache[url] = true;
return true;
} catch (error) {
// Clear any partial cache entry on error
delete imageCache[url];
return false;
// Preload image when component mounts
useEffect(() => {
if (featuredContent?.poster && !imageCache[featuredContent.poster]) {
const preloadImage = async () => {
try {
await imageCacheService.getCachedImageUrl(featuredContent.poster!);
imageCache[featuredContent.poster!] = true;
} catch (error) {
logger.error('Failed to preload featured image:', error);
}
};
preloadImage();
}
};
}, [featuredContent?.poster]);
// Reset logo error state when content changes
// TMDB data fetching removed due to API limitations
// Fetch logo when featured content changes
useEffect(() => {
setLogoLoadError(false);
}, [featuredContent?.id]);
// Fetch logo based on preference
useEffect(() => {
if (!featuredContent || logoFetchInProgress.current) return;
const fetchLogo = async () => {
logoFetchInProgress.current = true;
if (!featuredContent || isLogoLoading) return;
setIsLogoLoading(true);
setLogoUrl(null);
try {
const contentId = featuredContent.id;
const contentData = featuredContent; // Use a clearer variable name
const currentLogo = contentData.logo;
// Get preferences
const logoPreference = settings.logoSourcePreference || 'metahub';
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
// Reset state for new fetch
setLogoUrl(null);
setLogoLoadError(false);
// Extract IDs
let imdbId: string | null = null;
if (contentData.id.startsWith('tt')) {
imdbId = contentData.id;
} else if ((contentData as any).imdbId) {
imdbId = (contentData as any).imdbId;
} else if ((contentData as any).externalIds?.imdb_id) {
imdbId = (contentData as any).externalIds.imdb_id;
// Use existing logo logic
if (featuredContent.logo) {
setLogoUrl(featuredContent.logo);
}
let tmdbId: string | null = null;
if (contentData.id.startsWith('tmdb:')) {
tmdbId = contentData.id.split(':')[1];
} else if ((contentData as any).tmdb_id) {
tmdbId = String((contentData as any).tmdb_id);
}
// If we only have IMDB ID, try to find TMDB ID proactively
if (imdbId && !tmdbId) {
try {
const tmdbService = TMDBService.getInstance();
const foundData = await tmdbService.findTMDBIdByIMDB(imdbId);
if (foundData) {
tmdbId = String(foundData);
}
} catch (findError) {
// logger.warn(`[FeaturedContent] Failed to find TMDB ID for ${imdbId}:`, findError);
}
}
const tmdbType = contentData.type === 'series' ? 'tv' : 'movie';
let finalLogoUrl: string | null = null;
let primaryAttempted = false;
let fallbackAttempted = false;
// --- Logo Fetching Logic ---
if (logoPreference === 'metahub') {
// Primary: Metahub (needs imdbId)
if (imdbId) {
primaryAttempted = true;
const metahubUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`;
try {
const response = await fetch(metahubUrl, { method: 'HEAD' });
if (response.ok) {
finalLogoUrl = metahubUrl;
}
} catch (error) { /* Log if needed */ }
}
// Fallback: TMDB (needs tmdbId)
if (!finalLogoUrl && tmdbId) {
fallbackAttempted = true;
try {
const tmdbService = TMDBService.getInstance();
const logoUrl = await tmdbService.getContentLogo(tmdbType, tmdbId, preferredLanguage);
if (logoUrl) {
finalLogoUrl = logoUrl;
}
} catch (error) { /* Log if needed */ }
}
} else { // logoPreference === 'tmdb'
// Primary: TMDB (needs tmdbId)
if (tmdbId) {
primaryAttempted = true;
try {
const tmdbService = TMDBService.getInstance();
const logoUrl = await tmdbService.getContentLogo(tmdbType, tmdbId, preferredLanguage);
if (logoUrl) {
finalLogoUrl = logoUrl;
}
} catch (error) { /* Log if needed */ }
}
// Fallback: Metahub (needs imdbId)
if (!finalLogoUrl && imdbId) {
fallbackAttempted = true;
const metahubUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`;
try {
const response = await fetch(metahubUrl, { method: 'HEAD' });
if (response.ok) {
finalLogoUrl = metahubUrl;
}
} catch (error) { /* Log if needed */ }
}
}
// --- Set Final Logo ---
if (finalLogoUrl) {
setLogoUrl(finalLogoUrl);
} else if (currentLogo) {
// Use existing logo only if primary and fallback failed or weren't applicable
setLogoUrl(currentLogo);
} else {
// No logo found from any source
setLogoLoadError(true);
// logger.warn(`[FeaturedContent] No logo found for ${contentData.name} (${contentId}) with preference ${logoPreference}. Primary attempted: ${primaryAttempted}, Fallback attempted: ${fallbackAttempted}`);
}
} catch (error) {
// logger.error('[FeaturedContent] Error in fetchLogo:', error);
setLogoLoadError(true);
logger.error('Error fetching logo:', error);
} finally {
logoFetchInProgress.current = false;
setIsLogoLoading(false);
}
};
// Trigger fetch when content changes
fetchLogo();
}, [featuredContent, settings.logoSourcePreference, settings.tmdbLanguagePreference]);
}, [featuredContent]);
// Load poster and logo
useEffect(() => {
if (!featuredContent) return;
const posterUrl = featuredContent.banner || featuredContent.poster;
const contentId = featuredContent.id;
const isContentChange = contentId !== prevContentIdRef.current;
// Enhanced content change detection and animations
if (isContentChange) {
// Animate out current content
if (prevContentIdRef.current) {
posterOpacity.value = withTiming(0, {
duration: 300,
easing: Easing.out(Easing.cubic)
});
posterScale.value = withTiming(0.95, {
duration: 300,
easing: Easing.out(Easing.cubic)
});
overlayOpacity.value = withTiming(0.6, {
duration: 300,
easing: Easing.out(Easing.cubic)
});
contentOpacity.value = withTiming(0.3, {
duration: 200,
easing: Easing.out(Easing.cubic)
});
buttonsOpacity.value = withTiming(0.3, {
duration: 200,
easing: Easing.out(Easing.cubic)
});
} else {
// Initial load - start from 0
posterOpacity.value = 0;
posterScale.value = 1.1;
overlayOpacity.value = 0;
contentOpacity.value = 0;
buttonsOpacity.value = 0;
}
logoOpacity.value = 0;
const handlePlayPress = () => {
if (featuredContent) {
navigation.navigate('Metadata', {
id: featuredContent.id,
type: featuredContent.type
});
}
prevContentIdRef.current = contentId;
// Set poster URL for immediate display
if (posterUrl) setBannerUrl(posterUrl);
// Load images with enhanced animations
const loadImages = async () => {
// Small delay to allow fade out animation to complete
await new Promise(resolve => setTimeout(resolve, isContentChange && prevContentIdRef.current ? 300 : 0));
// Load poster with enhanced transition
if (posterUrl) {
const posterSuccess = await preloadImage(posterUrl);
if (posterSuccess) {
// Animate in new poster with scale and fade
posterScale.value = withTiming(1, {
duration: 800,
easing: Easing.out(Easing.cubic)
});
posterOpacity.value = withTiming(1, {
duration: 700,
easing: Easing.out(Easing.cubic)
});
overlayOpacity.value = withTiming(0.15, {
duration: 600,
easing: Easing.out(Easing.cubic)
});
// Animate content back in with delay
contentOpacity.value = withDelay(200, withTiming(1, {
duration: 600,
easing: Easing.out(Easing.cubic)
}));
buttonsOpacity.value = withDelay(400, withTiming(1, {
duration: 500,
easing: Easing.out(Easing.cubic)
}));
}
}
// Load logo if available with enhanced timing
if (logoUrl) {
const logoSuccess = await preloadImage(logoUrl);
if (logoSuccess) {
logoOpacity.value = withDelay(500, withTiming(1, {
duration: 600,
easing: Easing.out(Easing.cubic)
}));
} else {
setLogoLoadError(true);
}
}
};
loadImages();
}, [featuredContent?.id, logoUrl]);
const onLogoLoadError = () => {
setLogoLoaded(true); // Treat error as "loaded" to stop spinner
setLogoError(true);
};
const handleInfoPress = () => {
if (featuredContent) {
navigation.navigate('Metadata', {
id: featuredContent.id,
type: featuredContent.type
});
id: featuredContent.id,
type: featuredContent.type
});
}
};
const formatGenres = (genres: string[] | undefined) => {
if (!genres || genres.length === 0) return '';
return genres.slice(0, 3).join(' • ');
};
if (!featuredContent) {
return <NoFeaturedContent />;
}
const posterUrl = featuredContent.poster;
const formattedGenres = formatGenres(featuredContent.genres);
return (
<Animated.View
entering={FadeIn.duration(400).easing(Easing.out(Easing.cubic))}
>
<TouchableOpacity
activeOpacity={0.95}
onPress={() => {
navigation.navigate('Metadata', {
id: featuredContent.id,
type: featuredContent.type
});
}}
style={styles.featuredContainer as ViewStyle}
<View style={styles.featuredContainer}>
{/* Background Image */}
<View style={styles.imageContainer}>
{posterUrl && !imageError ? (
<ExpoImage
source={{ uri: posterUrl }}
style={styles.featuredImage}
contentFit="cover"
cachePolicy="memory-disk"
transition={300}
onLoad={() => setImageLoaded(true)}
onError={() => setImageError(true)}
placeholder={{ uri: 'https://via.placeholder.com/400x600' }}
placeholderContentFit="cover"
/>
) : (
<View style={[styles.backgroundFallback, { backgroundColor: currentTheme.colors.elevation1 }]}>
<MaterialIcons name="movie" size={64} color={currentTheme.colors.mediumEmphasis} />
</View>
)}
</View>
{/* Content Overlay */}
<View style={styles.contentOverlay} />
{/* Gradient Overlay */}
<LinearGradient
colors={[
'transparent',
'rgba(0,0,0,0.3)',
'rgba(0,0,0,0.7)',
'rgba(0,0,0,0.9)'
]}
locations={[0, 0.4, 0.7, 1]}
style={styles.featuredGradient}
>
<Animated.View style={[styles.imageContainer, posterAnimatedStyle]}>
<ImageBackground
source={{ uri: bannerUrl || featuredContent.poster }}
style={styles.featuredImage as ViewStyle}
resizeMode="cover"
>
{/* Subtle content overlay for better readability */}
<Animated.View style={[styles.contentOverlay, overlayAnimatedStyle]} />
<View style={styles.featuredContentContainer}>
{/* Logo or Title */}
{logoUrl && !isLogoLoading ? (
<ExpoImage
source={{ uri: logoUrl }}
style={styles.featuredLogo}
contentFit="contain"
cachePolicy="memory-disk"
transition={200}
/>
) : (
<Text style={[styles.featuredTitleText, { color: '#FFFFFF' }]} numberOfLines={2}>
{featuredContent.name}
</Text>
)}
<LinearGradient
colors={[
'rgba(0,0,0,0.1)',
'rgba(0,0,0,0.2)',
'rgba(0,0,0,0.4)',
'rgba(0,0,0,0.8)',
currentTheme.colors.darkBackground,
]}
locations={[0, 0.2, 0.5, 0.8, 1]}
style={styles.featuredGradient as ViewStyle}
{/* Genres */}
{formattedGenres && (
<View style={styles.genreContainer}>
<Text style={[styles.genreText, { color: '#FFFFFF' }]}>
{formattedGenres}
</Text>
</View>
)}
{/* Action Buttons */}
<View style={styles.featuredButtons}>
{/* Play Button */}
<TouchableOpacity
style={[styles.playButton, { backgroundColor: '#FFFFFF' }]}
onPress={handlePlayPress}
activeOpacity={0.8}
>
<Animated.View
style={[styles.featuredContentContainer as ViewStyle, contentAnimatedStyle]}
>
{logoUrl && !logoLoadError ? (
<Animated.View style={logoAnimatedStyle}>
<ExpoImage
source={{ uri: logoUrl }}
style={styles.featuredLogo as ImageStyle}
contentFit="contain"
cachePolicy="memory"
transition={300}
recyclingKey={`logo-${featuredContent.id}`}
onError={onLogoLoadError}
/>
</Animated.View>
) : (
<Text style={[styles.featuredTitleText as TextStyle, { color: currentTheme.colors.highEmphasis }]}>
{featuredContent.name}
</Text>
)}
<View style={styles.genreContainer as ViewStyle}>
{featuredContent.genres?.slice(0, 3).map((genre, index, array) => (
<React.Fragment key={index}>
<Text style={[styles.genreText as TextStyle, { color: currentTheme.colors.white }]}>
{genre}
</Text>
{index < array.length - 1 && (
<Text style={[styles.genreDot as TextStyle, { color: currentTheme.colors.white }]}></Text>
)}
</React.Fragment>
))}
</View>
</Animated.View>
<MaterialIcons name="play-arrow" size={20} color="#000000" />
<Text style={[styles.playButtonText, { color: '#000000' }]}>Play</Text>
</TouchableOpacity>
<Animated.View style={[styles.featuredButtons as ViewStyle, buttonsAnimatedStyle]}>
<TouchableOpacity
style={styles.myListButton as ViewStyle}
onPress={handleSaveToLibrary}
activeOpacity={0.7}
>
<MaterialIcons
name={isSaved ? "bookmark" : "bookmark-border"}
size={24}
color={currentTheme.colors.white}
/>
<Text style={[styles.myListButtonText as TextStyle, { color: currentTheme.colors.white }]}>
{isSaved ? "Saved" : "Save"}
</Text>
</TouchableOpacity>
{/* My List Button */}
<TouchableOpacity
style={styles.myListButton}
onPress={handleSaveToLibrary}
activeOpacity={0.7}
>
<MaterialIcons
name={isSaved ? "check" : "add"}
size={20}
color="#FFFFFF"
/>
<Text style={[styles.myListButtonText, { color: '#FFFFFF' }]}>
{isSaved ? 'Saved' : 'My List'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
onPress={() => {
if (featuredContent) {
navigation.navigate('Streams', {
id: featuredContent.id,
type: featuredContent.type
});
}
}}
activeOpacity={0.8}
>
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
Play
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.infoButton as ViewStyle}
onPress={handleInfoPress}
activeOpacity={0.7}
>
<MaterialIcons name="info-outline" size={24} color={currentTheme.colors.white} />
<Text style={[styles.infoButtonText as TextStyle, { color: currentTheme.colors.white }]}>
Info
</Text>
</TouchableOpacity>
</Animated.View>
</LinearGradient>
</ImageBackground>
</Animated.View>
</TouchableOpacity>
</Animated.View>
{/* Info Button */}
<TouchableOpacity
style={styles.infoButton}
onPress={handleInfoPress}
activeOpacity={0.7}
>
<MaterialIcons name="info-outline" size={20} color="#FFFFFF" />
<Text style={[styles.infoButtonText, { color: '#FFFFFF' }]}>Info</Text>
</TouchableOpacity>
</View>
</View>
</LinearGradient>
</View>
);
};
const styles = StyleSheet.create({
featuredContainer: {
width: '100%',
height: height * 0.55, // Slightly taller for better proportions
height: height * 0.55,
marginTop: 0,
marginBottom: 12,
position: 'relative',
@ -596,7 +273,7 @@ const styles = StyleSheet.create({
featuredImage: {
width: '100%',
height: '100%',
transform: [{ scale: 1.05 }], // Subtle zoom for depth
transform: [{ scale: 1.05 }],
},
backgroundFallback: {
position: 'absolute',
@ -724,6 +401,23 @@ const styles = StyleSheet.create({
zIndex: 1,
pointerEvents: 'none',
},
noContentText: {
fontSize: 16,
fontWeight: '500',
marginTop: 16,
marginBottom: 20,
textAlign: 'center',
},
exploreButton: {
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
},
exploreButtonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
},
});
export default React.memo(FeaturedContent);
export default React.memo(FeaturedContent);

View file

@ -20,7 +20,7 @@ import { tmdbService } from '../../services/tmdbService';
import { useLibrary } from '../../hooks/useLibrary';
import { RootStackParamList } from '../../navigation/AppNavigator';
import { parseISO, isThisWeek, format, isAfter, isBefore } from 'date-fns';
import Animated, { FadeIn, FadeInRight } from 'react-native-reanimated';
// Removed react-native-reanimated import
import { useCalendarData } from '../../hooks/useCalendarData';
const { width } = Dimensions.get('window');
@ -109,10 +109,7 @@ export const ThisWeekSection = React.memo(() => {
item.poster);
return (
<Animated.View
entering={FadeInRight.delay(index * 50).duration(300)}
style={styles.episodeItemContainer}
>
<View style={styles.episodeItemContainer}>
<TouchableOpacity
style={[
styles.episodeItem,
@ -177,12 +174,12 @@ export const ThisWeekSection = React.memo(() => {
</LinearGradient>
</View>
</TouchableOpacity>
</Animated.View>
</View>
);
};
return (
<Animated.View entering={FadeIn.duration(300)} style={styles.container}>
<View style={styles.container}>
<View style={styles.header}>
<View style={styles.titleContainer}>
<Text style={[styles.title, { color: currentTheme.colors.text }]}>This Week</Text>
@ -206,7 +203,7 @@ export const ThisWeekSection = React.memo(() => {
snapToAlignment="start"
ItemSeparatorComponent={() => <View style={{ width: 16 }} />}
/>
</Animated.View>
</View>
);
});
@ -337,4 +334,4 @@ const styles = StyleSheet.create({
marginLeft: 6,
letterSpacing: 0.3,
},
});
});

View file

@ -7,19 +7,11 @@ import {
ActivityIndicator,
Dimensions,
Platform,
Modal,
} from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { BlurView } from 'expo-blur';
import { Image } from 'expo-image';
import Animated, {
FadeIn,
FadeOut,
useAnimatedStyle,
useSharedValue,
withTiming,
withSpring,
runOnJS,
} from 'react-native-reanimated';
import { LinearGradient } from 'expo-linear-gradient';
import { useTheme } from '../../contexts/ThemeContext';
import { Cast } from '../../types/cast';
@ -54,419 +46,360 @@ export const CastDetailsModal: React.FC<CastDetailsModalProps> = ({
const { currentTheme } = useTheme();
const [personDetails, setPersonDetails] = useState<PersonDetails | null>(null);
const [loading, setLoading] = useState(false);
const [hasFetched, setHasFetched] = useState(false);
const modalOpacity = useSharedValue(0);
const modalScale = useSharedValue(0.9);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (visible && castMember) {
modalOpacity.value = withTiming(1, { duration: 250 });
modalScale.value = withSpring(1, { damping: 20, stiffness: 200 });
if (!hasFetched || personDetails?.id !== castMember.id) {
fetchPersonDetails();
}
} else {
modalOpacity.value = withTiming(0, { duration: 200 });
modalScale.value = withTiming(0.9, { duration: 200 });
if (!visible) {
setHasFetched(false);
setPersonDetails(null);
}
if (visible && castMember?.id) {
fetchPersonDetails();
}
}, [visible, castMember]);
}, [visible, castMember?.id]);
const fetchPersonDetails = async () => {
if (!castMember || loading) return;
if (!castMember?.id) return;
setLoading(true);
setError(null);
try {
const details = await tmdbService.getPersonDetails(castMember.id);
setPersonDetails(details);
setHasFetched(true);
} catch (error) {
console.error('Error fetching person details:', error);
} catch (err) {
console.error('Error fetching person details:', err);
setError('Failed to load cast member details');
} finally {
setLoading(false);
}
};
const modalStyle = useAnimatedStyle(() => ({
opacity: modalOpacity.value,
transform: [{ scale: modalScale.value }],
}));
const handleClose = () => {
modalOpacity.value = withTiming(0, { duration: 200 });
modalScale.value = withTiming(0.9, { duration: 200 }, () => {
runOnJS(onClose)();
});
};
const formatDate = (dateString: string | null) => {
if (!dateString) return null;
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
try {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
} catch {
return dateString;
}
};
const calculateAge = (birthday: string | null) => {
if (!birthday) return null;
const today = new Date();
const birthDate = new Date(birthday);
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
try {
const birthDate = new Date(birthday);
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
} catch {
return null;
}
return age;
};
if (!visible || !castMember) return null;
return (
<Animated.View
entering={FadeIn.duration(250)}
exiting={FadeOut.duration(200)}
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.85)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 9999,
padding: 20,
}}
<Modal
visible={visible}
transparent
animationType="fade"
onRequestClose={onClose}
>
<TouchableOpacity
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
onPress={handleClose}
activeOpacity={1}
/>
<Animated.View
style={[
{
width: MODAL_WIDTH,
height: MODAL_HEIGHT,
overflow: 'hidden',
borderRadius: 24,
backgroundColor: Platform.OS === 'android'
? 'rgba(20, 20, 20, 0.95)'
: 'transparent',
},
modalStyle,
]}
>
{Platform.OS === 'ios' ? (
<BlurView
intensity={100}
tint="dark"
style={{
width: '100%',
height: '100%',
backgroundColor: 'rgba(20, 20, 20, 0.8)',
}}
>
{renderContent()}
</BlurView>
) : (
renderContent()
)}
</Animated.View>
</Animated.View>
);
function renderContent() {
return (
<>
{/* Header */}
<LinearGradient
colors={[
currentTheme.colors.primary + 'DD',
currentTheme.colors.primaryVariant + 'CC',
]}
style={{
padding: 20,
paddingTop: 24,
}}
>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={{
width: 60,
height: 60,
borderRadius: 30,
overflow: 'hidden',
marginRight: 16,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
}}>
{castMember.profile_path ? (
<Image
source={{
uri: `https://image.tmdb.org/t/p/w185${castMember.profile_path}`,
}}
style={{ width: '100%', height: '100%' }}
contentFit="cover"
/>
) : (
<View style={{
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
}}>
<Text style={{
color: '#fff',
fontSize: 18,
fontWeight: '700',
}}>
{castMember.name.split(' ').reduce((prev: string, current: string) => prev + current[0], '').substring(0, 2)}
</Text>
</View>
)}
</View>
<View style={{ flex: 1 }}>
<Text style={{
color: '#fff',
fontSize: 18,
fontWeight: '800',
marginBottom: 4,
}} numberOfLines={2}>
{castMember.name}
</Text>
{castMember.character && (
<Text style={{
color: 'rgba(255, 255, 255, 0.8)',
fontSize: 14,
fontWeight: '500',
}} numberOfLines={2}>
as {castMember.character}
</Text>
)}
</View>
<TouchableOpacity
style={{
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
justifyContent: 'center',
alignItems: 'center',
}}
onPress={handleClose}
activeOpacity={0.7}
>
<MaterialIcons name="close" size={20} color="#fff" />
<View style={styles.overlay}>
<TouchableOpacity
style={styles.backdrop}
activeOpacity={1}
onPress={onClose}
/>
<View style={[styles.modalContainer, { backgroundColor: currentTheme.colors.darkBackground }]}>
{Platform.OS === 'ios' ? (
<BlurView intensity={80} style={styles.blurBackground} tint="dark" />
) : (
<View style={[styles.androidBackground, { backgroundColor: currentTheme.colors.darkBackground }]} />
)}
{/* Header */}
<View style={styles.header}>
<Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}>
Cast Details
</Text>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<MaterialIcons name="close" size={24} color={currentTheme.colors.highEmphasis} />
</TouchableOpacity>
</View>
</LinearGradient>
{/* Content */}
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ padding: 20 }}
showsVerticalScrollIndicator={false}
>
{loading ? (
<View style={{
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 40,
}}>
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
<Text style={{
color: 'rgba(255, 255, 255, 0.7)',
fontSize: 14,
marginTop: 12,
}}>
Loading details...
</Text>
</View>
) : (
<View>
{/* Quick Info */}
{(personDetails?.known_for_department || personDetails?.birthday || personDetails?.place_of_birth) && (
<View style={{
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: 16,
padding: 16,
marginBottom: 20,
}}>
{personDetails?.known_for_department && (
<View style={{
flexDirection: 'row',
alignItems: 'center',
marginBottom: personDetails?.birthday || personDetails?.place_of_birth ? 12 : 0
}}>
<MaterialIcons name="work" size={16} color={currentTheme.colors.primary} />
<Text style={{
color: 'rgba(255, 255, 255, 0.7)',
fontSize: 12,
marginLeft: 8,
marginRight: 12,
}}>
Department
</Text>
<Text style={{
color: '#fff',
fontSize: 14,
fontWeight: '600',
}}>
{personDetails.known_for_department}
</Text>
</View>
)}
{personDetails?.birthday && (
<View style={{
flexDirection: 'row',
alignItems: 'center',
marginBottom: personDetails?.place_of_birth ? 12 : 0
}}>
<MaterialIcons name="cake" size={16} color="#22C55E" />
<Text style={{
color: 'rgba(255, 255, 255, 0.7)',
fontSize: 12,
marginLeft: 8,
marginRight: 12,
}}>
Age
</Text>
<Text style={{
color: '#fff',
fontSize: 14,
fontWeight: '600',
}}>
{calculateAge(personDetails.birthday)} years old
</Text>
</View>
)}
{personDetails?.place_of_birth && (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="place" size={16} color="#F59E0B" />
<Text style={{
color: 'rgba(255, 255, 255, 0.7)',
fontSize: 12,
marginLeft: 8,
marginRight: 12,
}}>
Born in
</Text>
<Text style={{
color: '#fff',
fontSize: 14,
fontWeight: '600',
flex: 1,
}}>
{personDetails.place_of_birth}
</Text>
</View>
)}
{personDetails?.birthday && (
<View style={{
marginTop: 12,
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: 'rgba(255, 255, 255, 0.1)',
}}>
<Text style={{
color: 'rgba(255, 255, 255, 0.7)',
fontSize: 12,
marginBottom: 4,
}}>
Born on {formatDate(personDetails.birthday)}
</Text>
</View>
)}
{/* Content */}
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
<Text style={[styles.loadingText, { color: currentTheme.colors.mediumEmphasis }]}>
Loading details...
</Text>
</View>
) : error ? (
<View style={styles.errorContainer}>
<MaterialIcons name="error-outline" size={48} color={currentTheme.colors.error} />
<Text style={[styles.errorText, { color: currentTheme.colors.error }]}>
{error}
</Text>
<TouchableOpacity onPress={fetchPersonDetails} style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]}>
<Text style={styles.retryButtonText}>Retry</Text>
</TouchableOpacity>
</View>
) : personDetails ? (
<View style={styles.detailsContainer}>
{/* Profile Image and Basic Info */}
<View style={styles.profileSection}>
<View style={styles.imageContainer}>
{personDetails.profile_path ? (
<Image
source={{ uri: `https://image.tmdb.org/t/p/w500${personDetails.profile_path}` }}
style={styles.profileImage}
contentFit="cover"
/>
) : (
<View style={[styles.placeholderImage, { backgroundColor: currentTheme.colors.elevation1 }]}>
<MaterialIcons name="person" size={60} color={currentTheme.colors.mediumEmphasis} />
</View>
)}
</View>
<View style={styles.basicInfo}>
<Text style={[styles.name, { color: currentTheme.colors.highEmphasis }]}>
{personDetails.name}
</Text>
<Text style={[styles.department, { color: currentTheme.colors.primary }]}>
{personDetails.known_for_department}
</Text>
{personDetails.birthday && (
<View style={styles.infoRow}>
<MaterialIcons name="cake" size={16} color={currentTheme.colors.mediumEmphasis} />
<Text style={[styles.infoText, { color: currentTheme.colors.mediumEmphasis }]}>
{formatDate(personDetails.birthday)}
{calculateAge(personDetails.birthday) && ` (${calculateAge(personDetails.birthday)} years old)`}
</Text>
</View>
)}
{personDetails.place_of_birth && (
<View style={styles.infoRow}>
<MaterialIcons name="place" size={16} color={currentTheme.colors.mediumEmphasis} />
<Text style={[styles.infoText, { color: currentTheme.colors.mediumEmphasis }]}>
{personDetails.place_of_birth}
</Text>
</View>
)}
</View>
</View>
)}
{/* Biography */}
{personDetails.biography && (
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>
Biography
</Text>
<Text style={[styles.biography, { color: currentTheme.colors.mediumEmphasis }]}>
{personDetails.biography}
</Text>
</View>
)}
{/* Also Known As */}
{personDetails.also_known_as && personDetails.also_known_as.length > 0 && (
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>
Also Known As
</Text>
<View style={styles.aliasContainer}>
{personDetails.also_known_as.slice(0, 5).map((alias, index) => (
<View key={index} style={[styles.aliasChip, { backgroundColor: currentTheme.colors.elevation1 }]}>
<Text style={[styles.aliasText, { color: currentTheme.colors.mediumEmphasis }]}>
{alias}
</Text>
</View>
))}
</View>
</View>
)}
</View>
) : null}
</ScrollView>
</View>
</View>
</Modal>
);
};
{/* Biography */}
{personDetails?.biography && (
<View style={{ marginBottom: 20 }}>
<Text style={{
color: '#fff',
fontSize: 16,
fontWeight: '700',
marginBottom: 12,
}}>
Biography
</Text>
<Text style={{
color: 'rgba(255, 255, 255, 0.9)',
fontSize: 14,
lineHeight: 20,
fontWeight: '400',
}}>
{personDetails.biography}
</Text>
</View>
)}
{/* Also Known As - Compact */}
{personDetails?.also_known_as && personDetails.also_known_as.length > 0 && (
<View>
<Text style={{
color: '#fff',
fontSize: 16,
fontWeight: '700',
marginBottom: 12,
}}>
Also Known As
</Text>
<Text style={{
color: 'rgba(255, 255, 255, 0.8)',
fontSize: 14,
lineHeight: 20,
}}>
{personDetails.also_known_as.slice(0, 4).join(' • ')}
</Text>
</View>
)}
{/* No details available */}
{!loading && !personDetails?.biography && !personDetails?.birthday && !personDetails?.place_of_birth && (
<View style={{
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 40,
}}>
<MaterialIcons name="info" size={32} color="rgba(255, 255, 255, 0.3)" />
<Text style={{
color: 'rgba(255, 255, 255, 0.7)',
fontSize: 14,
marginTop: 12,
textAlign: 'center',
}}>
No additional details available
</Text>
</View>
)}
</View>
)}
</ScrollView>
</>
);
}
const styles = {
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
justifyContent: 'center' as const,
alignItems: 'center' as const,
},
backdrop: {
position: 'absolute' as const,
top: 0,
left: 0,
right: 0,
bottom: 0,
},
modalContainer: {
width: MODAL_WIDTH,
height: MODAL_HEIGHT,
borderRadius: 16,
overflow: 'hidden' as const,
elevation: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
},
blurBackground: {
position: 'absolute' as const,
top: 0,
left: 0,
right: 0,
bottom: 0,
},
androidBackground: {
position: 'absolute' as const,
top: 0,
left: 0,
right: 0,
bottom: 0,
opacity: 0.95,
},
header: {
flexDirection: 'row' as const,
justifyContent: 'space-between' as const,
alignItems: 'center' as const,
padding: 20,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
},
headerTitle: {
fontSize: 20,
fontWeight: '700' as const,
},
closeButton: {
padding: 4,
},
content: {
flex: 1,
padding: 20,
},
loadingContainer: {
flex: 1,
justifyContent: 'center' as const,
alignItems: 'center' as const,
paddingVertical: 40,
},
loadingText: {
marginTop: 16,
fontSize: 16,
},
errorContainer: {
flex: 1,
justifyContent: 'center' as const,
alignItems: 'center' as const,
paddingVertical: 40,
},
errorText: {
marginTop: 16,
fontSize: 16,
textAlign: 'center' as const,
marginBottom: 20,
},
retryButton: {
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 8,
},
retryButtonText: {
color: '#fff',
fontWeight: '600' as const,
},
detailsContainer: {
flex: 1,
},
profileSection: {
flexDirection: 'row' as const,
marginBottom: 24,
},
imageContainer: {
marginRight: 16,
},
profileImage: {
width: 100,
height: 150,
borderRadius: 8,
},
placeholderImage: {
width: 100,
height: 150,
borderRadius: 8,
justifyContent: 'center' as const,
alignItems: 'center' as const,
},
basicInfo: {
flex: 1,
justifyContent: 'flex-start' as const,
},
name: {
fontSize: 24,
fontWeight: '700' as const,
marginBottom: 4,
},
department: {
fontSize: 16,
fontWeight: '600' as const,
marginBottom: 12,
},
infoRow: {
flexDirection: 'row' as const,
alignItems: 'center' as const,
marginBottom: 8,
},
infoText: {
fontSize: 14,
marginLeft: 8,
flex: 1,
},
section: {
marginBottom: 24,
},
sectionTitle: {
fontSize: 18,
fontWeight: '700' as const,
marginBottom: 12,
},
biography: {
fontSize: 14,
lineHeight: 20,
},
aliasContainer: {
flexDirection: 'row' as const,
flexWrap: 'wrap' as const,
gap: 8,
},
aliasChip: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
},
aliasText: {
fontSize: 12,
fontWeight: '500' as const,
},
};
export default CastDetailsModal;

View file

@ -11,11 +11,6 @@ import { BlurView as ExpoBlurView } from 'expo-blur';
import { BlurView as CommunityBlurView } from '@react-native-community/blur';
import { MaterialIcons } from '@expo/vector-icons';
import { Image } from 'expo-image';
import Animated, {
useAnimatedStyle,
interpolate,
Extrapolate,
} from 'react-native-reanimated';
import { useTheme } from '../../contexts/ThemeContext';
import { logger } from '../../utils/logger';
@ -27,9 +22,6 @@ interface FloatingHeaderProps {
handleBack: () => void;
handleToggleLibrary: () => void;
inLibrary: boolean;
headerOpacity: Animated.SharedValue<number>;
headerElementsY: Animated.SharedValue<number>;
headerElementsOpacity: Animated.SharedValue<number>;
safeAreaTop: number;
setLogoLoadError: (error: boolean) => void;
}
@ -40,37 +32,20 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
handleBack,
handleToggleLibrary,
inLibrary,
headerOpacity,
headerElementsY,
headerElementsOpacity,
safeAreaTop,
setLogoLoadError,
}) => {
const { currentTheme } = useTheme();
// Animated styles for the header
const headerAnimatedStyle = useAnimatedStyle(() => ({
opacity: headerOpacity.value,
transform: [
{ translateY: interpolate(headerOpacity.value, [0, 1], [-20, 0], Extrapolate.CLAMP) }
]
}));
// Animated style for header elements
const headerElementsStyle = useAnimatedStyle(() => ({
opacity: headerElementsOpacity.value,
transform: [{ translateY: headerElementsY.value }]
}));
return (
<Animated.View style={[styles.floatingHeader, headerAnimatedStyle]}>
<View style={styles.floatingHeader}>
{Platform.OS === 'ios' ? (
<ExpoBlurView
intensity={50}
tint="dark"
style={[styles.blurContainer, { paddingTop: Math.max(safeAreaTop * 0.8, safeAreaTop - 6) }]}
>
<Animated.View style={[styles.floatingHeaderContent, headerElementsStyle]}>
<View style={styles.floatingHeaderContent}>
<TouchableOpacity
style={styles.backButton}
onPress={handleBack}
@ -111,7 +86,7 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
color={currentTheme.colors.highEmphasis}
/>
</TouchableOpacity>
</Animated.View>
</View>
</ExpoBlurView>
) : (
<View style={[styles.blurContainer, { paddingTop: Math.max(safeAreaTop * 0.8, safeAreaTop - 6) }]}>
@ -121,7 +96,7 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
blurAmount={15}
reducedTransparencyFallbackColor="rgba(20, 20, 20, 0.9)"
/>
<Animated.View style={[styles.floatingHeaderContent, headerElementsStyle]}>
<View style={styles.floatingHeaderContent}>
<TouchableOpacity
style={styles.backButton}
onPress={handleBack}
@ -162,11 +137,11 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
color={currentTheme.colors.highEmphasis}
/>
</TouchableOpacity>
</Animated.View>
</View>
</View>
)}
{Platform.OS === 'ios' && <View style={[styles.headerBottomBorder, { backgroundColor: 'rgba(255,255,255,0.15)' }]} />}
</Animated.View>
</View>
);
};
@ -240,4 +215,4 @@ const styles = StyleSheet.create({
},
});
export default React.memo(FloatingHeader);
export default React.memo(FloatingHeader);

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,7 @@
import React from 'react';
import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Dimensions } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import Animated, {
FadeIn,
FadeOut,
SlideInRight,
SlideOutRight,
} from 'react-native-reanimated';
// Removed react-native-reanimated imports
import { styles } from '../utils/playerStyles';
import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes';
import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils';
@ -89,9 +84,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
return (
<>
{/* Backdrop */}
<Animated.View
entering={FadeIn.duration(200)}
exiting={FadeOut.duration(150)}
<View
style={{
position: 'absolute',
top: 0,
@ -107,12 +100,10 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
onPress={handleClose}
activeOpacity={1}
/>
</Animated.View>
</View>
{/* Side Menu */}
<Animated.View
entering={SlideInRight.duration(300)}
exiting={SlideOutRight.duration(250)}
<View
style={{
position: 'absolute',
top: 0,
@ -527,7 +518,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
</TouchableOpacity>
</View>
</ScrollView>
</Animated.View>
</View>
</>
);
};
@ -539,4 +530,4 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
);
};
export default SubtitleModals;
export default SubtitleModals;

View file

@ -1,202 +0,0 @@
import { useEffect } from 'react';
import { Dimensions } from 'react-native';
import {
useSharedValue,
withTiming,
withSpring,
Easing,
useAnimatedScrollHandler,
runOnUI,
cancelAnimation,
} from 'react-native-reanimated';
const { width, height } = Dimensions.get('window');
// Highly optimized animation configurations
const fastSpring = {
damping: 15,
mass: 0.8,
stiffness: 150,
};
const ultraFastSpring = {
damping: 12,
mass: 0.6,
stiffness: 200,
};
// Ultra-optimized easing functions
const easings = {
fast: Easing.out(Easing.quad),
ultraFast: Easing.out(Easing.linear),
natural: Easing.bezier(0.2, 0, 0.2, 1),
};
export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) => {
// Consolidated entrance animations - start with visible values for Android compatibility
const screenOpacity = useSharedValue(1);
const contentOpacity = useSharedValue(1);
// Combined hero animations
const heroOpacity = useSharedValue(1);
const heroScale = useSharedValue(1); // Start at 1 for Android compatibility
const heroHeightValue = useSharedValue(height * 0.5);
// Combined UI element animations
const uiElementsOpacity = useSharedValue(1);
const uiElementsTranslateY = useSharedValue(0);
// Progress animation - simplified to single value
const progressOpacity = useSharedValue(0);
// Scroll values - minimal
const scrollY = useSharedValue(0);
const headerProgress = useSharedValue(0); // Single value for all header animations
// Static header elements Y for performance
const staticHeaderElementsY = useSharedValue(0);
// Ultra-fast entrance sequence - batch animations for better performance
useEffect(() => {
// Batch all entrance animations to run simultaneously with safety
const enterAnimations = () => {
'worklet';
try {
// Start with slightly reduced values and animate to full visibility
screenOpacity.value = withTiming(1, {
duration: 250,
easing: easings.fast
});
heroOpacity.value = withTiming(1, {
duration: 300,
easing: easings.fast
});
heroScale.value = withSpring(1, ultraFastSpring);
uiElementsOpacity.value = withTiming(1, {
duration: 400,
easing: easings.natural
});
uiElementsTranslateY.value = withSpring(0, fastSpring);
contentOpacity.value = withTiming(1, {
duration: 350,
easing: easings.fast
});
} catch (error) {
// Silently handle any animation errors
console.warn('Animation error in enterAnimations:', error);
}
};
// Use runOnUI for better performance with error handling
try {
runOnUI(enterAnimations)();
} catch (error) {
console.warn('Failed to run enter animations:', error);
}
}, []);
// Optimized watch progress animation with safety
useEffect(() => {
const hasProgress = watchProgress && watchProgress.duration > 0;
const updateProgress = () => {
'worklet';
try {
progressOpacity.value = withTiming(hasProgress ? 1 : 0, {
duration: hasProgress ? 200 : 150,
easing: easings.fast
});
} catch (error) {
console.warn('Animation error in updateProgress:', error);
}
};
try {
runOnUI(updateProgress)();
} catch (error) {
console.warn('Failed to run progress animation:', error);
}
}, [watchProgress]);
// Cleanup function to cancel animations
useEffect(() => {
return () => {
try {
cancelAnimation(screenOpacity);
cancelAnimation(contentOpacity);
cancelAnimation(heroOpacity);
cancelAnimation(heroScale);
cancelAnimation(uiElementsOpacity);
cancelAnimation(uiElementsTranslateY);
cancelAnimation(progressOpacity);
cancelAnimation(scrollY);
cancelAnimation(headerProgress);
cancelAnimation(staticHeaderElementsY);
} catch (error) {
console.warn('Error canceling animations:', error);
}
};
}, []);
// Ultra-optimized scroll handler with minimal calculations and safety
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {
'worklet';
try {
const rawScrollY = event.contentOffset.y;
scrollY.value = rawScrollY;
// Single calculation for header threshold
const threshold = height * 0.4 - safeAreaTop;
const progress = rawScrollY > threshold ? 1 : 0;
// Use single progress value for all header animations
if (headerProgress.value !== progress) {
headerProgress.value = withTiming(progress, {
duration: progress ? 200 : 150,
easing: easings.ultraFast
});
}
} catch (error) {
console.warn('Animation error in scroll handler:', error);
}
},
});
return {
// Optimized shared values - reduced count
screenOpacity,
contentOpacity,
heroOpacity,
heroScale,
uiElementsOpacity,
uiElementsTranslateY,
progressOpacity,
scrollY,
headerProgress,
// Direct shared value references for compatibility
heroHeight: heroHeightValue,
logoOpacity: uiElementsOpacity,
buttonsOpacity: uiElementsOpacity,
buttonsTranslateY: uiElementsTranslateY,
contentTranslateY: uiElementsTranslateY,
watchProgressOpacity: progressOpacity,
watchProgressWidth: progressOpacity, // Reuse for width animation
headerOpacity: headerProgress,
headerElementsY: staticHeaderElementsY,
headerElementsOpacity: headerProgress,
// Functions
scrollHandler,
animateLogo: () => {}, // Simplified - no separate logo animation
};
};

View file

@ -13,12 +13,13 @@ import {
ActivityIndicator,
Platform,
ScrollView,
TVEventHandler,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons';
import { Image } from 'expo-image';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
// Removed react-native-reanimated import
import { LinearGradient } from 'expo-linear-gradient';
import { catalogService } from '../services/catalogService';
import type { StreamingContent } from '../services/catalogService';
@ -203,7 +204,7 @@ const SkeletonLoader = () => {
const LibraryScreen = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const isDarkMode = useColorScheme() === 'dark';
const { width } = useWindowDimensions();
const { width, height } = useWindowDimensions();
const [loading, setLoading] = useState(true);
const [libraryItems, setLibraryItems] = useState<LibraryItem[]>([]);
const [filter, setFilter] = useState<'all' | 'movies' | 'series'>('all');
@ -212,6 +213,22 @@ const LibraryScreen = () => {
const insets = useSafeAreaInsets();
const { currentTheme } = useTheme();
// TV-optimized grid calculations
const isTV = Platform.isTV || width > 1200;
const getNumColumns = () => {
if (isTV) {
if (width >= 1920) return 6; // 4K TVs
if (width >= 1280) return 5; // HD TVs
return 4; // Smaller TVs
}
return 2; // Mobile/tablet
};
const numColumns = getNumColumns();
const itemSpacing = isTV ? 24 : 16;
const containerPadding = isTV ? 48 : 12;
const itemWidth = (width - (containerPadding * 2) - (itemSpacing * (numColumns - 1))) / numColumns;
// Trakt integration
const {
isAuthenticated: traktAuthenticated,
@ -270,6 +287,51 @@ const LibraryScreen = () => {
};
}, []);
// TV Event Handler for remote control navigation
useEffect(() => {
if (!isTV) return;
const handleTVEvent = (evt: any) => {
if (evt && evt.eventType === 'focus') {
// Handle focus events for TV navigation
console.log('TV Focus Event:', evt);
} else if (evt && evt.eventType === 'blur') {
// Handle blur events
console.log('TV Blur Event:', evt);
} else if (evt && evt.eventType === 'select') {
// Handle select/enter button press
console.log('TV Select Event:', evt);
} else if (evt && evt.eventType === 'longSelect') {
// Handle long press on select button
console.log('TV Long Select Event:', evt);
} else if (evt && evt.eventType === 'left') {
// Handle left arrow navigation
console.log('TV Left Event:', evt);
} else if (evt && evt.eventType === 'right') {
// Handle right arrow navigation
console.log('TV Right Event:', evt);
} else if (evt && evt.eventType === 'up') {
// Handle up arrow navigation
console.log('TV Up Event:', evt);
} else if (evt && evt.eventType === 'down') {
// Handle down arrow navigation
console.log('TV Down Event:', evt);
} else if (evt && evt.eventType === 'playPause') {
// Handle play/pause button
console.log('TV Play/Pause Event:', evt);
} else if (evt && evt.eventType === 'menu') {
// Handle menu button - could show filters or options
console.log('TV Menu Event:', evt);
}
};
const subscription = TVEventHandler.addListener(handleTVEvent);
return () => {
subscription?.remove();
};
}, [isTV]);
const filteredItems = libraryItems.filter(item => {
if (filter === 'all') return true;
if (filter === 'movies') return item.type === 'movie';
@ -328,15 +390,37 @@ const LibraryScreen = () => {
return folders.filter(folder => folder.itemCount > 0);
}, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]);
const itemWidth = (width - 48) / 2; // 2 items per row with padding
// Use the TV-optimized itemWidth from above instead of hardcoded calculation
const renderItem = ({ item }: { item: LibraryItem }) => (
<TouchableOpacity
style={[styles.itemContainer, { width: itemWidth }]}
style={[
styles.itemContainer,
{
width: itemWidth,
marginHorizontal: itemSpacing / 2,
}
]}
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
activeOpacity={0.7}
// TV optimizations
hasTVPreferredFocus={false}
tvParallaxProperties={{
enabled: true,
shiftDistanceX: 2.0,
shiftDistanceY: 2.0,
tiltAngle: 0.05,
magnification: 1.1,
}}
>
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
<View style={[
styles.posterContainer,
{
shadowColor: currentTheme.colors.black,
// TV-optimized dimensions
height: itemWidth * 1.5, // 2:3 aspect ratio
}
]}>
<Image
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
style={styles.poster}
@ -348,13 +432,24 @@ const LibraryScreen = () => {
style={styles.posterGradient}
>
<Text
style={[styles.itemTitle, { color: currentTheme.colors.white }]}
style={[
styles.itemTitle,
{
color: currentTheme.colors.white,
fontSize: width > 1920 ? 18 : width > 1280 ? 16 : 15, // Responsive font size
}
]}
numberOfLines={2}
>
{item.name}
</Text>
{item.lastWatched && (
<Text style={styles.lastWatched}>
<Text style={[
styles.lastWatched,
{
fontSize: width > 1920 ? 14 : width > 1280 ? 13 : 12, // Responsive font size
}
]}>
{item.lastWatched}
</Text>
)}
@ -365,7 +460,11 @@ const LibraryScreen = () => {
<View
style={[
styles.progressBar,
{ width: `${item.progress * 100}%`, backgroundColor: currentTheme.colors.primary }
{
width: `${item.progress * 100}%`,
backgroundColor: currentTheme.colors.primary,
height: width > 1920 ? 6 : 4, // Larger progress bar for TV
}
]}
/>
</View>
@ -374,11 +473,17 @@ const LibraryScreen = () => {
<View style={styles.badgeContainer}>
<MaterialIcons
name="live-tv"
size={14}
size={width > 1920 ? 18 : width > 1280 ? 16 : 14} // Responsive icon size
color={currentTheme.colors.white}
style={{ marginRight: 4 }}
/>
<Text style={[styles.badgeText, { color: currentTheme.colors.white }]}>Series</Text>
<Text style={[
styles.badgeText,
{
color: currentTheme.colors.white,
fontSize: width > 1920 ? 12 : width > 1280 ? 11 : 10, // Responsive font size
}
]}>Series</Text>
</View>
)}
</View>
@ -388,31 +493,69 @@ const LibraryScreen = () => {
// Render individual Trakt collection folder
const renderTraktCollectionFolder = ({ folder }: { folder: TraktFolder }) => (
<TouchableOpacity
style={[styles.itemContainer, { width: itemWidth }]}
style={[
styles.itemContainer,
{
width: itemWidth,
marginHorizontal: itemSpacing / 2,
}
]}
onPress={() => {
setSelectedTraktFolder(folder.id);
loadAllCollections(); // Load all collections when entering a specific folder
}}
activeOpacity={0.7}
// TV optimizations
hasTVPreferredFocus={false}
tvParallaxProperties={{
enabled: true,
shiftDistanceX: 2.0,
shiftDistanceY: 2.0,
tiltAngle: 0.05,
magnification: 1.1,
}}
>
<View style={[styles.posterContainer, styles.folderContainer, { shadowColor: currentTheme.colors.black }]}>
<View style={[
styles.posterContainer,
styles.folderContainer,
{
shadowColor: currentTheme.colors.black,
height: itemWidth * 1.5, // 2:3 aspect ratio
}
]}>
<LinearGradient
colors={folder.gradient}
style={styles.folderGradient}
>
<MaterialIcons
name={folder.icon}
size={60}
size={width > 1920 ? 80 : width > 1280 ? 70 : 60} // Responsive icon size
color={currentTheme.colors.white}
style={{ marginBottom: 12 }}
style={{ marginBottom: width > 1920 ? 16 : 12 }}
/>
<Text style={[styles.folderTitle, { color: currentTheme.colors.white }]}>
<Text style={[
styles.folderTitle,
{
color: currentTheme.colors.white,
fontSize: width > 1920 ? 22 : width > 1280 ? 20 : 18, // Responsive font size
}
]}>
{folder.name}
</Text>
<Text style={styles.folderCount}>
<Text style={[
styles.folderCount,
{
fontSize: width > 1920 ? 14 : width > 1280 ? 13 : 12, // Responsive font size
}
]}>
{folder.itemCount} items
</Text>
<Text style={styles.folderSubtitle}>
<Text style={[
styles.folderSubtitle,
{
fontSize: width > 1920 ? 14 : width > 1280 ? 13 : 12, // Responsive font size
}
]}>
{folder.description}
</Text>
</LinearGradient>
@ -859,14 +1002,28 @@ const LibraryScreen = () => {
return renderItem({ item: item as LibraryItem });
}}
keyExtractor={item => item.id}
numColumns={2}
contentContainerStyle={styles.listContainer}
numColumns={numColumns}
contentContainerStyle={[
styles.listContainer,
{
paddingHorizontal: containerPadding,
paddingVertical: width > 1920 ? 24 : width > 1280 ? 20 : 16,
paddingBottom: width > 1920 ? 120 : 90,
}
]}
showsVerticalScrollIndicator={false}
columnWrapperStyle={styles.columnWrapper}
initialNumToRender={6}
maxToRenderPerBatch={6}
columnWrapperStyle={numColumns > 1 ? [
styles.columnWrapper,
{
marginBottom: width > 1920 ? 24 : width > 1280 ? 20 : 16,
}
] : undefined}
initialNumToRender={numColumns * 3}
maxToRenderPerBatch={numColumns * 2}
windowSize={5}
removeClippedSubviews={Platform.OS === 'android'}
// TV optimizations
getItemLayout={undefined} // Let FlatList calculate for TV focus
/>
);
};
@ -1017,7 +1174,7 @@ const styles = StyleSheet.create({
paddingBottom: 90,
},
columnWrapper: {
justifyContent: 'space-between',
justifyContent: 'flex-start',
marginBottom: 16,
},
skeletonContainer: {
@ -1248,4 +1405,4 @@ const styles = StyleSheet.create({
},
});
export default LibraryScreen;
export default LibraryScreen;

View file

@ -7,6 +7,7 @@ import {
ActivityIndicator,
Dimensions,
TouchableOpacity,
ScrollView,
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useRoute, useNavigation } from '@react-navigation/native';
@ -21,13 +22,6 @@ import { MovieContent } from '../components/metadata/MovieContent';
import { MoreLikeThisSection } from '../components/metadata/MoreLikeThisSection';
import { RatingsSection } from '../components/metadata/RatingsSection';
import { RouteParams, Episode } from '../types/metadata';
import Animated, {
useAnimatedStyle,
interpolate,
Extrapolate,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { RouteProp } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator';
@ -38,7 +32,6 @@ import { MetadataLoadingScreen } from '../components/loading/MetadataLoadingScre
import HeroSection from '../components/metadata/HeroSection';
import FloatingHeader from '../components/metadata/FloatingHeader';
import MetadataDetails from '../components/metadata/MetadataDetails';
import { useMetadataAnimations } from '../hooks/useMetadataAnimations';
import { useMetadataAssets } from '../hooks/useMetadataAssets';
import { useWatchProgress } from '../hooks/useWatchProgress';
import { TraktService, TraktPlaybackItem } from '../services/traktService';
@ -59,7 +52,7 @@ const MetadataScreen: React.FC = () => {
const [isContentReady, setIsContentReady] = useState(false);
const [showCastModal, setShowCastModal] = useState(false);
const [selectedCastMember, setSelectedCastMember] = useState<any>(null);
const transitionOpacity = useSharedValue(1);
// Removed animation state
const {
metadata,
@ -84,7 +77,6 @@ const MetadataScreen: React.FC = () => {
// Optimized hooks with memoization
const watchProgressData = useWatchProgress(id, type as 'movie' | 'series', episodeId, episodes);
const assetData = useMetadataAssets(metadata, id, type, imdbId, settings, setMetadata);
const animations = useMetadataAnimations(safeAreaTop, watchProgressData.watchProgress);
// Fetch and log Trakt progress data when entering the screen
useEffect(() => {
@ -192,11 +184,10 @@ const MetadataScreen: React.FC = () => {
useEffect(() => {
if (isReady) {
setIsContentReady(true);
transitionOpacity.value = withTiming(1, { duration: 50 });
// Removed animation logic
} else if (!isReady && isContentReady) {
setIsContentReady(false);
transitionOpacity.value = 0;
}
setIsContentReady(false);
}
}, [isReady, isContentReady]);
// Optimized callback functions with reduced dependencies
@ -318,19 +309,7 @@ const MetadataScreen: React.FC = () => {
setShowCastModal(true);
}, []);
// Ultra-optimized animated styles - minimal calculations
const containerStyle = useAnimatedStyle(() => ({
opacity: animations.screenOpacity.value,
}), []);
const contentStyle = useAnimatedStyle(() => ({
opacity: animations.contentOpacity.value,
transform: [{ translateY: animations.uiElementsTranslateY.value }]
}), []);
const transitionStyle = useAnimatedStyle(() => ({
opacity: transitionOpacity.value,
}), []);
// Removed animated styles
// Memoized error component for performance
const ErrorComponent = useMemo(() => {
@ -377,7 +356,7 @@ const MetadataScreen: React.FC = () => {
return (
<SafeAreaView
style={[containerStyle, styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}
style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}
edges={['bottom']}
>
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" animated />
@ -390,19 +369,14 @@ const MetadataScreen: React.FC = () => {
logoLoadError={assetData.logoLoadError}
handleBack={handleBack}
handleToggleLibrary={handleToggleLibrary}
headerElementsY={animations.headerElementsY}
inLibrary={inLibrary}
headerOpacity={animations.headerOpacity}
headerElementsOpacity={animations.headerElementsOpacity}
safeAreaTop={safeAreaTop}
setLogoLoadError={assetData.setLogoLoadError}
/>
<Animated.ScrollView
<ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
onScroll={animations.scrollHandler}
scrollEventThrottle={16}
bounces={false}
overScrollMode="never"
contentContainerStyle={styles.scrollContent}
@ -413,14 +387,6 @@ const MetadataScreen: React.FC = () => {
bannerImage={assetData.bannerImage}
loadingBanner={assetData.loadingBanner}
logoLoadError={assetData.logoLoadError}
scrollY={animations.scrollY}
heroHeight={animations.heroHeight}
heroOpacity={animations.heroOpacity}
logoOpacity={animations.logoOpacity}
buttonsOpacity={animations.buttonsOpacity}
buttonsTranslateY={animations.buttonsTranslateY}
watchProgressOpacity={animations.watchProgressOpacity}
watchProgressWidth={animations.watchProgressWidth}
watchProgress={watchProgressData.watchProgress}
type={type as 'movie' | 'series'}
getEpisodeDetails={watchProgressData.getEpisodeDetails}
@ -436,7 +402,7 @@ const MetadataScreen: React.FC = () => {
/>
{/* Main Content - Optimized */}
<Animated.View style={contentStyle}>
<View>
<MetadataDetails
metadata={metadata}
imdbId={imdbId}
@ -475,8 +441,8 @@ const MetadataScreen: React.FC = () => {
) : (
metadata && <MovieContent metadata={metadata} />
)}
</Animated.View>
</Animated.ScrollView>
</View>
</ScrollView>
</>
)}

View file

@ -13,14 +13,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context';
import { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
FadeInDown,
FadeInUp,
} from 'react-native-reanimated';
// Removed react-native-reanimated imports
import { useTheme } from '../contexts/ThemeContext';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator';
@ -77,18 +70,14 @@ const OnboardingScreen = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const [currentIndex, setCurrentIndex] = useState(0);
const flatListRef = useRef<FlatList>(null);
const progressValue = useSharedValue(0);
const animatedProgressStyle = useAnimatedStyle(() => ({
width: withSpring(`${((currentIndex + 1) / onboardingData.length) * 100}%`),
}));
// Removed animated progress values
const handleNext = () => {
if (currentIndex < onboardingData.length - 1) {
const nextIndex = currentIndex + 1;
setCurrentIndex(nextIndex);
flatListRef.current?.scrollToIndex({ index: nextIndex, animated: true });
progressValue.value = (nextIndex + 1) / onboardingData.length;
// Removed progress animation
} else {
handleGetStarted();
}
@ -125,22 +114,16 @@ const OnboardingScreen = () => {
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Animated.View
entering={FadeInDown.delay(300).duration(800)}
style={styles.iconWrapper}
>
<View style={styles.iconWrapper}>
<MaterialIcons
name={item.icon}
size={80}
color="white"
/>
</Animated.View>
</View>
</LinearGradient>
<Animated.View
entering={FadeInUp.delay(500).duration(800)}
style={styles.textContainer}
>
<View style={styles.textContainer}>
<Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]}>
{item.title}
</Text>
@ -150,7 +133,7 @@ const OnboardingScreen = () => {
<Text style={[styles.description, { color: currentTheme.colors.mediumEmphasis }]}>
{item.description}
</Text>
</Animated.View>
</View>
</View>
);
};
@ -188,11 +171,10 @@ const OnboardingScreen = () => {
{/* Progress Bar */}
<View style={[styles.progressContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
<Animated.View
<View
style={[
styles.progressBar,
{ backgroundColor: currentTheme.colors.primary },
animatedProgressStyle
{ backgroundColor: currentTheme.colors.primary, width: `${((currentIndex + 1) / onboardingData.length) * 100}%` }
]}
/>
</View>
@ -370,4 +352,4 @@ const styles = StyleSheet.create({
},
});
export default OnboardingScreen;
export default OnboardingScreen;