removed reanimated
This commit is contained in:
parent
604b38ba20
commit
332cf99f67
14 changed files with 1100 additions and 2262 deletions
2
App.tsx
2
App.tsx
|
|
@ -19,7 +19,7 @@ import AppNavigator, {
|
||||||
CustomNavigationDarkTheme,
|
CustomNavigationDarkTheme,
|
||||||
CustomDarkTheme
|
CustomDarkTheme
|
||||||
} from './src/navigation/AppNavigator';
|
} from './src/navigation/AppNavigator';
|
||||||
import 'react-native-reanimated';
|
// Removed react-native-reanimated import
|
||||||
import { CatalogProvider } from './src/contexts/CatalogContext';
|
import { CatalogProvider } from './src/contexts/CatalogContext';
|
||||||
import { GenreProvider } from './src/contexts/GenreContext';
|
import { GenreProvider } from './src/contexts/GenreContext';
|
||||||
import { TraktProvider } from './src/contexts/TraktContext';
|
import { TraktProvider } from './src/contexts/TraktContext';
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
"react-native-gesture-handler": "~2.20.2",
|
"react-native-gesture-handler": "~2.20.2",
|
||||||
"react-native-immersive-mode": "^2.0.2",
|
"react-native-immersive-mode": "^2.0.2",
|
||||||
"react-native-paper": "^5.13.1",
|
"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-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
"react-native-svg": "^15.11.2",
|
"react-native-svg": "^15.11.2",
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
Alert,
|
Alert,
|
||||||
ActivityIndicator
|
ActivityIndicator
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
// Removed react-native-reanimated import
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { NavigationProp } from '@react-navigation/native';
|
import { NavigationProp } from '@react-navigation/native';
|
||||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||||
|
|
@ -572,7 +572,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View entering={FadeIn.duration(300).delay(150)} style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={styles.titleContainer}>
|
<View style={styles.titleContainer}>
|
||||||
<Text style={[styles.title, { color: currentTheme.colors.text }]}>Continue Watching</Text>
|
<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 */}
|
{/* Delete Indicator Overlay */}
|
||||||
{deletingItemId === item.id && (
|
{deletingItemId === item.id && (
|
||||||
<Animated.View
|
<View style={styles.deletingOverlay}>
|
||||||
entering={FadeIn.duration(200)}
|
|
||||||
exiting={FadeOut.duration(200)}
|
|
||||||
style={styles.deletingOverlay}
|
|
||||||
>
|
|
||||||
<ActivityIndicator size="large" color="#FFFFFF" />
|
<ActivityIndicator size="large" color="#FFFFFF" />
|
||||||
</Animated.View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -700,7 +696,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
snapToAlignment="start"
|
snapToAlignment="start"
|
||||||
ItemSeparatorComponent={() => <View style={{ width: 16 }} />}
|
ItemSeparatorComponent={() => <View style={{ width: 16 }} />}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</View>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -13,19 +13,6 @@ import {
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { Image as ExpoImage } from 'expo-image';
|
import { Image as ExpoImage } from 'expo-image';
|
||||||
import { colors } from '../../styles/colors';
|
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';
|
import { StreamingContent } from '../../services/catalogService';
|
||||||
|
|
||||||
interface DropUpMenuProps {
|
interface DropUpMenuProps {
|
||||||
|
|
@ -36,144 +23,92 @@ interface DropUpMenuProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) => {
|
export const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) => {
|
||||||
const translateY = useSharedValue(300);
|
|
||||||
const opacity = useSharedValue(0);
|
|
||||||
const isDarkMode = useColorScheme() === 'dark';
|
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 = [
|
const menuOptions = [
|
||||||
{
|
{ id: 'play', label: 'Play', icon: 'play-arrow' },
|
||||||
icon: item.inLibrary ? 'bookmark' : 'bookmark-border',
|
{ id: 'info', label: 'More Info', icon: 'info-outline' },
|
||||||
label: item.inLibrary ? 'Remove from Library' : 'Add to Library',
|
{ id: 'save', label: 'Add to My List', icon: 'bookmark-border' },
|
||||||
action: 'library'
|
{ id: 'share', label: 'Share', icon: 'share' },
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'check-circle',
|
|
||||||
label: 'Mark as Watched',
|
|
||||||
action: 'watched'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'playlist-add',
|
|
||||||
label: 'Add to Playlist',
|
|
||||||
action: 'playlist'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'share',
|
|
||||||
label: 'Share',
|
|
||||||
action: 'share'
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const backgroundColor = isDarkMode ? '#1A1A1A' : '#FFFFFF';
|
const handleOptionPress = (optionId: string) => {
|
||||||
|
onOptionSelect(optionId);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
transparent
|
transparent
|
||||||
animationType="none"
|
animationType="slide"
|
||||||
onRequestClose={onClose}
|
onRequestClose={onClose}
|
||||||
>
|
>
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<View style={styles.modalOverlay}>
|
||||||
<Animated.View style={[styles.modalOverlay, overlayStyle]}>
|
<Pressable style={styles.modalOverlayPressable} onPress={onClose} />
|
||||||
<Pressable style={styles.modalOverlayPressable} onPress={onClose} />
|
|
||||||
<GestureDetector gesture={gesture}>
|
<View style={[
|
||||||
<Animated.View style={[styles.menuContainer, menuStyle, { backgroundColor }]}>
|
styles.menuContainer,
|
||||||
<View style={styles.dragHandle} />
|
{ backgroundColor: isDarkMode ? colors.darkBackground : colors.lightBackground }
|
||||||
<View style={styles.menuHeader}>
|
]}>
|
||||||
<ExpoImage
|
{/* Drag Handle */}
|
||||||
source={{ uri: item.poster }}
|
<View style={styles.dragHandle} />
|
||||||
style={styles.menuPoster}
|
|
||||||
contentFit="cover"
|
{/* 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={[
|
||||||
<Text style={[styles.menuTitle, { color: isDarkMode ? '#FFFFFF' : '#000000' }]}>
|
styles.menuOptionText,
|
||||||
{item.name}
|
{ color: isDarkMode ? colors.white : colors.black }
|
||||||
</Text>
|
]}>
|
||||||
{item.year && (
|
{option.label}
|
||||||
<Text style={[styles.menuYear, { color: isDarkMode ? '#999999' : '#666666' }]}>
|
</Text>
|
||||||
{item.year}
|
</TouchableOpacity>
|
||||||
</Text>
|
))}
|
||||||
)}
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</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>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,6 @@ import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { Image as ExpoImage } from 'expo-image';
|
import { Image as ExpoImage } from 'expo-image';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import Animated, {
|
|
||||||
FadeIn,
|
|
||||||
useAnimatedStyle,
|
|
||||||
useSharedValue,
|
|
||||||
withTiming,
|
|
||||||
Easing,
|
|
||||||
withDelay
|
|
||||||
} from 'react-native-reanimated';
|
|
||||||
import { StreamingContent } from '../../services/catalogService';
|
import { StreamingContent } from '../../services/catalogService';
|
||||||
import { SkeletonFeatured } from './SkeletonLoaders';
|
import { SkeletonFeatured } from './SkeletonLoaders';
|
||||||
import { isValidMetahubLogo, hasValidLogoFormat, isMetahubUrl, isTmdbUrl } from '../../utils/logoUtils';
|
import { isValidMetahubLogo, hasValidLogoFormat, isMetahubUrl, isTmdbUrl } from '../../utils/logoUtils';
|
||||||
|
|
@ -49,70 +41,18 @@ const NoFeaturedContent = () => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const { currentTheme } = useTheme();
|
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 (
|
return (
|
||||||
<View style={styles.noContentContainer}>
|
<View style={[styles.featuredContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||||
<MaterialIcons name="theaters" size={48} color={currentTheme.colors.mediumEmphasis} />
|
<View style={styles.backgroundFallback}>
|
||||||
<Text style={styles.noContentTitle}>No Featured Content</Text>
|
<MaterialIcons name="movie" size={64} color={currentTheme.colors.mediumEmphasis} />
|
||||||
<Text style={styles.noContentText}>
|
<Text style={[styles.noContentText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
Install addons with catalogs or change the content source in your settings.
|
No featured content available
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.noContentButtons}>
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]}
|
style={[styles.exploreButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||||
onPress={() => navigation.navigate('Addons')}
|
onPress={() => navigation.navigate('Search')}
|
||||||
>
|
>
|
||||||
<Text style={[styles.noContentButtonText, { color: currentTheme.colors.white }]}>Install Addons</Text>
|
<Text style={styles.exploreButtonText}>Explore Content</Text>
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.noContentButton}
|
|
||||||
onPress={() => navigation.navigate('HomeScreenSettings')}
|
|
||||||
>
|
|
||||||
<Text style={styles.noContentButtonText}>Settings</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -122,456 +62,193 @@ const NoFeaturedContent = () => {
|
||||||
const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: FeaturedContentProps) => {
|
const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: FeaturedContentProps) => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const { currentTheme } = useTheme();
|
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 { settings } = useSettings();
|
||||||
const logoOpacity = useSharedValue(0);
|
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
||||||
const bannerOpacity = useSharedValue(0);
|
const [isLogoLoading, setIsLogoLoading] = useState(false);
|
||||||
const posterOpacity = useSharedValue(0);
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
const prevContentIdRef = useRef<string | null>(null);
|
const [imageError, setImageError] = useState(false);
|
||||||
// Add state for tracking logo load errors
|
// Removed TMDB service integration
|
||||||
const [logoLoadError, setLogoLoadError] = useState(false);
|
|
||||||
// Add a ref to track logo fetch in progress
|
|
||||||
const logoFetchInProgress = useRef<boolean>(false);
|
|
||||||
|
|
||||||
// Enhanced poster transition animations
|
// Preload image when component mounts
|
||||||
const posterScale = useSharedValue(1);
|
useEffect(() => {
|
||||||
const posterTranslateY = useSharedValue(0);
|
if (featuredContent?.poster && !imageCache[featuredContent.poster]) {
|
||||||
const overlayOpacity = useSharedValue(0.15);
|
const preloadImage = async () => {
|
||||||
|
try {
|
||||||
// Animation values
|
await imageCacheService.getCachedImageUrl(featuredContent.poster!);
|
||||||
const posterAnimatedStyle = useAnimatedStyle(() => ({
|
imageCache[featuredContent.poster!] = true;
|
||||||
opacity: posterOpacity.value,
|
} catch (error) {
|
||||||
transform: [
|
logger.error('Failed to preload featured image:', error);
|
||||||
{ scale: posterScale.value },
|
}
|
||||||
{ translateY: posterTranslateY.value }
|
};
|
||||||
],
|
preloadImage();
|
||||||
}));
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
};
|
}, [featuredContent?.poster]);
|
||||||
|
|
||||||
// Reset logo error state when content changes
|
// TMDB data fetching removed due to API limitations
|
||||||
|
|
||||||
|
// Fetch logo when featured content changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLogoLoadError(false);
|
|
||||||
}, [featuredContent?.id]);
|
|
||||||
|
|
||||||
// Fetch logo based on preference
|
|
||||||
useEffect(() => {
|
|
||||||
if (!featuredContent || logoFetchInProgress.current) return;
|
|
||||||
|
|
||||||
const fetchLogo = async () => {
|
const fetchLogo = async () => {
|
||||||
logoFetchInProgress.current = true;
|
if (!featuredContent || isLogoLoading) return;
|
||||||
|
|
||||||
|
setIsLogoLoading(true);
|
||||||
|
setLogoUrl(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const contentId = featuredContent.id;
|
// Use existing logo logic
|
||||||
const contentData = featuredContent; // Use a clearer variable name
|
if (featuredContent.logo) {
|
||||||
const currentLogo = contentData.logo;
|
setLogoUrl(featuredContent.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
// logger.error('[FeaturedContent] Error in fetchLogo:', error);
|
logger.error('Error fetching logo:', error);
|
||||||
setLogoLoadError(true);
|
|
||||||
} finally {
|
} finally {
|
||||||
logoFetchInProgress.current = false;
|
setIsLogoLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Trigger fetch when content changes
|
|
||||||
fetchLogo();
|
fetchLogo();
|
||||||
}, [featuredContent, settings.logoSourcePreference, settings.tmdbLanguagePreference]);
|
}, [featuredContent]);
|
||||||
|
|
||||||
// Load poster and logo
|
const handlePlayPress = () => {
|
||||||
useEffect(() => {
|
if (featuredContent) {
|
||||||
if (!featuredContent) return;
|
navigation.navigate('Metadata', {
|
||||||
|
id: featuredContent.id,
|
||||||
const posterUrl = featuredContent.banner || featuredContent.poster;
|
type: featuredContent.type
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = () => {
|
const handleInfoPress = () => {
|
||||||
if (featuredContent) {
|
if (featuredContent) {
|
||||||
navigation.navigate('Metadata', {
|
navigation.navigate('Metadata', {
|
||||||
id: featuredContent.id,
|
id: featuredContent.id,
|
||||||
type: featuredContent.type
|
type: featuredContent.type
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatGenres = (genres: string[] | undefined) => {
|
||||||
|
if (!genres || genres.length === 0) return '';
|
||||||
|
return genres.slice(0, 3).join(' • ');
|
||||||
|
};
|
||||||
|
|
||||||
if (!featuredContent) {
|
if (!featuredContent) {
|
||||||
return <NoFeaturedContent />;
|
return <NoFeaturedContent />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const posterUrl = featuredContent.poster;
|
||||||
|
const formattedGenres = formatGenres(featuredContent.genres);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<View style={styles.featuredContainer}>
|
||||||
entering={FadeIn.duration(400).easing(Easing.out(Easing.cubic))}
|
{/* Background Image */}
|
||||||
>
|
<View style={styles.imageContainer}>
|
||||||
<TouchableOpacity
|
{posterUrl && !imageError ? (
|
||||||
activeOpacity={0.95}
|
<ExpoImage
|
||||||
onPress={() => {
|
source={{ uri: posterUrl }}
|
||||||
navigation.navigate('Metadata', {
|
style={styles.featuredImage}
|
||||||
id: featuredContent.id,
|
contentFit="cover"
|
||||||
type: featuredContent.type
|
cachePolicy="memory-disk"
|
||||||
});
|
transition={300}
|
||||||
}}
|
onLoad={() => setImageLoaded(true)}
|
||||||
style={styles.featuredContainer as ViewStyle}
|
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]}>
|
<View style={styles.featuredContentContainer}>
|
||||||
<ImageBackground
|
{/* Logo or Title */}
|
||||||
source={{ uri: bannerUrl || featuredContent.poster }}
|
{logoUrl && !isLogoLoading ? (
|
||||||
style={styles.featuredImage as ViewStyle}
|
<ExpoImage
|
||||||
resizeMode="cover"
|
source={{ uri: logoUrl }}
|
||||||
>
|
style={styles.featuredLogo}
|
||||||
{/* Subtle content overlay for better readability */}
|
contentFit="contain"
|
||||||
<Animated.View style={[styles.contentOverlay, overlayAnimatedStyle]} />
|
cachePolicy="memory-disk"
|
||||||
|
transition={200}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text style={[styles.featuredTitleText, { color: '#FFFFFF' }]} numberOfLines={2}>
|
||||||
|
{featuredContent.name}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
<LinearGradient
|
{/* Genres */}
|
||||||
colors={[
|
{formattedGenres && (
|
||||||
'rgba(0,0,0,0.1)',
|
<View style={styles.genreContainer}>
|
||||||
'rgba(0,0,0,0.2)',
|
<Text style={[styles.genreText, { color: '#FFFFFF' }]}>
|
||||||
'rgba(0,0,0,0.4)',
|
{formattedGenres}
|
||||||
'rgba(0,0,0,0.8)',
|
</Text>
|
||||||
currentTheme.colors.darkBackground,
|
</View>
|
||||||
]}
|
)}
|
||||||
locations={[0, 0.2, 0.5, 0.8, 1]}
|
|
||||||
style={styles.featuredGradient as ViewStyle}
|
{/* Action Buttons */}
|
||||||
|
<View style={styles.featuredButtons}>
|
||||||
|
{/* Play Button */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.playButton, { backgroundColor: '#FFFFFF' }]}
|
||||||
|
onPress={handlePlayPress}
|
||||||
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<Animated.View
|
<MaterialIcons name="play-arrow" size={20} color="#000000" />
|
||||||
style={[styles.featuredContentContainer as ViewStyle, contentAnimatedStyle]}
|
<Text style={[styles.playButtonText, { color: '#000000' }]}>Play</Text>
|
||||||
>
|
</TouchableOpacity>
|
||||||
{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>
|
|
||||||
|
|
||||||
<Animated.View style={[styles.featuredButtons as ViewStyle, buttonsAnimatedStyle]}>
|
{/* My List Button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.myListButton as ViewStyle}
|
style={styles.myListButton}
|
||||||
onPress={handleSaveToLibrary}
|
onPress={handleSaveToLibrary}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={isSaved ? "bookmark" : "bookmark-border"}
|
name={isSaved ? "check" : "add"}
|
||||||
size={24}
|
size={20}
|
||||||
color={currentTheme.colors.white}
|
color="#FFFFFF"
|
||||||
/>
|
/>
|
||||||
<Text style={[styles.myListButtonText as TextStyle, { color: currentTheme.colors.white }]}>
|
<Text style={[styles.myListButtonText, { color: '#FFFFFF' }]}>
|
||||||
{isSaved ? "Saved" : "Save"}
|
{isSaved ? 'Saved' : 'My List'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
{/* Info Button */}
|
||||||
style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
|
<TouchableOpacity
|
||||||
onPress={() => {
|
style={styles.infoButton}
|
||||||
if (featuredContent) {
|
onPress={handleInfoPress}
|
||||||
navigation.navigate('Streams', {
|
activeOpacity={0.7}
|
||||||
id: featuredContent.id,
|
>
|
||||||
type: featuredContent.type
|
<MaterialIcons name="info-outline" size={20} color="#FFFFFF" />
|
||||||
});
|
<Text style={[styles.infoButtonText, { color: '#FFFFFF' }]}>Info</Text>
|
||||||
}
|
</TouchableOpacity>
|
||||||
}}
|
</View>
|
||||||
activeOpacity={0.8}
|
</View>
|
||||||
>
|
</LinearGradient>
|
||||||
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
|
</View>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
featuredContainer: {
|
featuredContainer: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: height * 0.55, // Slightly taller for better proportions
|
height: height * 0.55,
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
|
@ -596,7 +273,7 @@ const styles = StyleSheet.create({
|
||||||
featuredImage: {
|
featuredImage: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
transform: [{ scale: 1.05 }], // Subtle zoom for depth
|
transform: [{ scale: 1.05 }],
|
||||||
},
|
},
|
||||||
backgroundFallback: {
|
backgroundFallback: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|
@ -724,6 +401,23 @@ const styles = StyleSheet.create({
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
pointerEvents: 'none',
|
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);
|
||||||
|
|
@ -20,7 +20,7 @@ import { tmdbService } from '../../services/tmdbService';
|
||||||
import { useLibrary } from '../../hooks/useLibrary';
|
import { useLibrary } from '../../hooks/useLibrary';
|
||||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||||
import { parseISO, isThisWeek, format, isAfter, isBefore } from 'date-fns';
|
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';
|
import { useCalendarData } from '../../hooks/useCalendarData';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
|
|
@ -109,10 +109,7 @@ export const ThisWeekSection = React.memo(() => {
|
||||||
item.poster);
|
item.poster);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<View style={styles.episodeItemContainer}>
|
||||||
entering={FadeInRight.delay(index * 50).duration(300)}
|
|
||||||
style={styles.episodeItemContainer}
|
|
||||||
>
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.episodeItem,
|
styles.episodeItem,
|
||||||
|
|
@ -177,12 +174,12 @@ export const ThisWeekSection = React.memo(() => {
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Animated.View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View entering={FadeIn.duration(300)} style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={styles.titleContainer}>
|
<View style={styles.titleContainer}>
|
||||||
<Text style={[styles.title, { color: currentTheme.colors.text }]}>This Week</Text>
|
<Text style={[styles.title, { color: currentTheme.colors.text }]}>This Week</Text>
|
||||||
|
|
@ -206,7 +203,7 @@ export const ThisWeekSection = React.memo(() => {
|
||||||
snapToAlignment="start"
|
snapToAlignment="start"
|
||||||
ItemSeparatorComponent={() => <View style={{ width: 16 }} />}
|
ItemSeparatorComponent={() => <View style={{ width: 16 }} />}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</View>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,11 @@ import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Platform,
|
Platform,
|
||||||
|
Modal,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { BlurView } from 'expo-blur';
|
import { BlurView } from 'expo-blur';
|
||||||
import { Image } from 'expo-image';
|
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 { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { Cast } from '../../types/cast';
|
import { Cast } from '../../types/cast';
|
||||||
|
|
@ -54,419 +46,360 @@ export const CastDetailsModal: React.FC<CastDetailsModalProps> = ({
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const [personDetails, setPersonDetails] = useState<PersonDetails | null>(null);
|
const [personDetails, setPersonDetails] = useState<PersonDetails | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [hasFetched, setHasFetched] = useState(false);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const modalOpacity = useSharedValue(0);
|
|
||||||
const modalScale = useSharedValue(0.9);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && castMember) {
|
if (visible && castMember?.id) {
|
||||||
modalOpacity.value = withTiming(1, { duration: 250 });
|
fetchPersonDetails();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [visible, castMember]);
|
}, [visible, castMember?.id]);
|
||||||
|
|
||||||
const fetchPersonDetails = async () => {
|
const fetchPersonDetails = async () => {
|
||||||
if (!castMember || loading) return;
|
if (!castMember?.id) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const details = await tmdbService.getPersonDetails(castMember.id);
|
const details = await tmdbService.getPersonDetails(castMember.id);
|
||||||
setPersonDetails(details);
|
setPersonDetails(details);
|
||||||
setHasFetched(true);
|
} catch (err) {
|
||||||
} catch (error) {
|
console.error('Error fetching person details:', err);
|
||||||
console.error('Error fetching person details:', error);
|
setError('Failed to load cast member details');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
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) => {
|
const formatDate = (dateString: string | null) => {
|
||||||
if (!dateString) return null;
|
if (!dateString) return null;
|
||||||
const date = new Date(dateString);
|
try {
|
||||||
return date.toLocaleDateString('en-US', {
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
month: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric'
|
||||||
});
|
});
|
||||||
|
} catch {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateAge = (birthday: string | null) => {
|
const calculateAge = (birthday: string | null) => {
|
||||||
if (!birthday) return null;
|
if (!birthday) return null;
|
||||||
const today = new Date();
|
try {
|
||||||
const birthDate = new Date(birthday);
|
const birthDate = new Date(birthday);
|
||||||
let age = today.getFullYear() - birthDate.getFullYear();
|
const today = new Date();
|
||||||
const monthDiff = today.getMonth() - birthDate.getMonth();
|
let age = today.getFullYear() - birthDate.getFullYear();
|
||||||
|
const monthDiff = today.getMonth() - birthDate.getMonth();
|
||||||
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
|
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
|
||||||
age--;
|
age--;
|
||||||
|
}
|
||||||
|
return age;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return age;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!visible || !castMember) return null;
|
if (!visible || !castMember) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Modal
|
||||||
entering={FadeIn.duration(250)}
|
visible={visible}
|
||||||
exiting={FadeOut.duration(200)}
|
transparent
|
||||||
style={{
|
animationType="fade"
|
||||||
position: 'absolute',
|
onRequestClose={onClose}
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.85)',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
zIndex: 9999,
|
|
||||||
padding: 20,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<View style={styles.overlay}>
|
||||||
style={{
|
<TouchableOpacity
|
||||||
position: 'absolute',
|
style={styles.backdrop}
|
||||||
top: 0,
|
activeOpacity={1}
|
||||||
left: 0,
|
onPress={onClose}
|
||||||
right: 0,
|
/>
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
onPress={handleClose}
|
|
||||||
activeOpacity={1}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Animated.View
|
<View style={[styles.modalContainer, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||||
style={[
|
{Platform.OS === 'ios' ? (
|
||||||
{
|
<BlurView intensity={80} style={styles.blurBackground} tint="dark" />
|
||||||
width: MODAL_WIDTH,
|
) : (
|
||||||
height: MODAL_HEIGHT,
|
<View style={[styles.androidBackground, { backgroundColor: currentTheme.colors.darkBackground }]} />
|
||||||
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() {
|
{/* Header */}
|
||||||
return (
|
<View style={styles.header}>
|
||||||
<>
|
<Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||||
{/* Header */}
|
Cast Details
|
||||||
<LinearGradient
|
</Text>
|
||||||
colors={[
|
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
||||||
currentTheme.colors.primary + 'DD',
|
<MaterialIcons name="close" size={24} color={currentTheme.colors.highEmphasis} />
|
||||||
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" />
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</LinearGradient>
|
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<ScrollView
|
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
|
||||||
style={{ flex: 1 }}
|
{loading ? (
|
||||||
contentContainerStyle={{ padding: 20 }}
|
<View style={styles.loadingContainer}>
|
||||||
showsVerticalScrollIndicator={false}
|
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||||
>
|
<Text style={[styles.loadingText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
{loading ? (
|
Loading details...
|
||||||
<View style={{
|
</Text>
|
||||||
alignItems: 'center',
|
</View>
|
||||||
justifyContent: 'center',
|
) : error ? (
|
||||||
paddingVertical: 40,
|
<View style={styles.errorContainer}>
|
||||||
}}>
|
<MaterialIcons name="error-outline" size={48} color={currentTheme.colors.error} />
|
||||||
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
<Text style={[styles.errorText, { color: currentTheme.colors.error }]}>
|
||||||
<Text style={{
|
{error}
|
||||||
color: 'rgba(255, 255, 255, 0.7)',
|
</Text>
|
||||||
fontSize: 14,
|
<TouchableOpacity onPress={fetchPersonDetails} style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]}>
|
||||||
marginTop: 12,
|
<Text style={styles.retryButtonText}>Retry</Text>
|
||||||
}}>
|
</TouchableOpacity>
|
||||||
Loading details...
|
</View>
|
||||||
</Text>
|
) : personDetails ? (
|
||||||
</View>
|
<View style={styles.detailsContainer}>
|
||||||
) : (
|
{/* Profile Image and Basic Info */}
|
||||||
<View>
|
<View style={styles.profileSection}>
|
||||||
{/* Quick Info */}
|
<View style={styles.imageContainer}>
|
||||||
{(personDetails?.known_for_department || personDetails?.birthday || personDetails?.place_of_birth) && (
|
{personDetails.profile_path ? (
|
||||||
<View style={{
|
<Image
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
source={{ uri: `https://image.tmdb.org/t/p/w500${personDetails.profile_path}` }}
|
||||||
borderRadius: 16,
|
style={styles.profileImage}
|
||||||
padding: 16,
|
contentFit="cover"
|
||||||
marginBottom: 20,
|
/>
|
||||||
}}>
|
) : (
|
||||||
{personDetails?.known_for_department && (
|
<View style={[styles.placeholderImage, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||||
<View style={{
|
<MaterialIcons name="person" size={60} color={currentTheme.colors.mediumEmphasis} />
|
||||||
flexDirection: 'row',
|
</View>
|
||||||
alignItems: 'center',
|
)}
|
||||||
marginBottom: personDetails?.birthday || personDetails?.place_of_birth ? 12 : 0
|
</View>
|
||||||
}}>
|
|
||||||
<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={styles.basicInfo}>
|
||||||
<View style={{
|
<Text style={[styles.name, { color: currentTheme.colors.highEmphasis }]}>
|
||||||
flexDirection: 'row',
|
{personDetails.name}
|
||||||
alignItems: 'center',
|
</Text>
|
||||||
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 && (
|
<Text style={[styles.department, { color: currentTheme.colors.primary }]}>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
{personDetails.known_for_department}
|
||||||
<MaterialIcons name="place" size={16} color="#F59E0B" />
|
</Text>
|
||||||
<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 && (
|
{personDetails.birthday && (
|
||||||
<View style={{
|
<View style={styles.infoRow}>
|
||||||
marginTop: 12,
|
<MaterialIcons name="cake" size={16} color={currentTheme.colors.mediumEmphasis} />
|
||||||
paddingTop: 12,
|
<Text style={[styles.infoText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
borderTopWidth: 1,
|
{formatDate(personDetails.birthday)}
|
||||||
borderTopColor: 'rgba(255, 255, 255, 0.1)',
|
{calculateAge(personDetails.birthday) && ` (${calculateAge(personDetails.birthday)} years old)`}
|
||||||
}}>
|
</Text>
|
||||||
<Text style={{
|
</View>
|
||||||
color: 'rgba(255, 255, 255, 0.7)',
|
)}
|
||||||
fontSize: 12,
|
|
||||||
marginBottom: 4,
|
{personDetails.place_of_birth && (
|
||||||
}}>
|
<View style={styles.infoRow}>
|
||||||
Born on {formatDate(personDetails.birthday)}
|
<MaterialIcons name="place" size={16} color={currentTheme.colors.mediumEmphasis} />
|
||||||
</Text>
|
<Text style={[styles.infoText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
</View>
|
{personDetails.place_of_birth}
|
||||||
)}
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Biography */}
|
{/* Biography */}
|
||||||
{personDetails?.biography && (
|
{personDetails.biography && (
|
||||||
<View style={{ marginBottom: 20 }}>
|
<View style={styles.section}>
|
||||||
<Text style={{
|
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||||
color: '#fff',
|
Biography
|
||||||
fontSize: 16,
|
</Text>
|
||||||
fontWeight: '700',
|
<Text style={[styles.biography, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
marginBottom: 12,
|
{personDetails.biography}
|
||||||
}}>
|
</Text>
|
||||||
Biography
|
</View>
|
||||||
</Text>
|
)}
|
||||||
<Text style={{
|
|
||||||
color: 'rgba(255, 255, 255, 0.9)',
|
|
||||||
fontSize: 14,
|
|
||||||
lineHeight: 20,
|
|
||||||
fontWeight: '400',
|
|
||||||
}}>
|
|
||||||
{personDetails.biography}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Also Known As - Compact */}
|
{/* Also Known As */}
|
||||||
{personDetails?.also_known_as && personDetails.also_known_as.length > 0 && (
|
{personDetails.also_known_as && personDetails.also_known_as.length > 0 && (
|
||||||
<View>
|
<View style={styles.section}>
|
||||||
<Text style={{
|
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||||
color: '#fff',
|
Also Known As
|
||||||
fontSize: 16,
|
</Text>
|
||||||
fontWeight: '700',
|
<View style={styles.aliasContainer}>
|
||||||
marginBottom: 12,
|
{personDetails.also_known_as.slice(0, 5).map((alias, index) => (
|
||||||
}}>
|
<View key={index} style={[styles.aliasChip, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||||
Also Known As
|
<Text style={[styles.aliasText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
</Text>
|
{alias}
|
||||||
<Text style={{
|
</Text>
|
||||||
color: 'rgba(255, 255, 255, 0.8)',
|
</View>
|
||||||
fontSize: 14,
|
))}
|
||||||
lineHeight: 20,
|
</View>
|
||||||
}}>
|
</View>
|
||||||
{personDetails.also_known_as.slice(0, 4).join(' • ')}
|
)}
|
||||||
</Text>
|
</View>
|
||||||
</View>
|
) : null}
|
||||||
)}
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
{/* No details available */}
|
const styles = {
|
||||||
{!loading && !personDetails?.biography && !personDetails?.birthday && !personDetails?.place_of_birth && (
|
overlay: {
|
||||||
<View style={{
|
flex: 1,
|
||||||
alignItems: 'center',
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center' as const,
|
||||||
paddingVertical: 40,
|
alignItems: 'center' as const,
|
||||||
}}>
|
},
|
||||||
<MaterialIcons name="info" size={32} color="rgba(255, 255, 255, 0.3)" />
|
backdrop: {
|
||||||
<Text style={{
|
position: 'absolute' as const,
|
||||||
color: 'rgba(255, 255, 255, 0.7)',
|
top: 0,
|
||||||
fontSize: 14,
|
left: 0,
|
||||||
marginTop: 12,
|
right: 0,
|
||||||
textAlign: 'center',
|
bottom: 0,
|
||||||
}}>
|
},
|
||||||
No additional details available
|
modalContainer: {
|
||||||
</Text>
|
width: MODAL_WIDTH,
|
||||||
</View>
|
height: MODAL_HEIGHT,
|
||||||
)}
|
borderRadius: 16,
|
||||||
</View>
|
overflow: 'hidden' as const,
|
||||||
)}
|
elevation: 8,
|
||||||
</ScrollView>
|
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;
|
export default CastDetailsModal;
|
||||||
|
|
@ -11,11 +11,6 @@ import { BlurView as ExpoBlurView } from 'expo-blur';
|
||||||
import { BlurView as CommunityBlurView } from '@react-native-community/blur';
|
import { BlurView as CommunityBlurView } from '@react-native-community/blur';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import Animated, {
|
|
||||||
useAnimatedStyle,
|
|
||||||
interpolate,
|
|
||||||
Extrapolate,
|
|
||||||
} from 'react-native-reanimated';
|
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
|
||||||
|
|
@ -27,9 +22,6 @@ interface FloatingHeaderProps {
|
||||||
handleBack: () => void;
|
handleBack: () => void;
|
||||||
handleToggleLibrary: () => void;
|
handleToggleLibrary: () => void;
|
||||||
inLibrary: boolean;
|
inLibrary: boolean;
|
||||||
headerOpacity: Animated.SharedValue<number>;
|
|
||||||
headerElementsY: Animated.SharedValue<number>;
|
|
||||||
headerElementsOpacity: Animated.SharedValue<number>;
|
|
||||||
safeAreaTop: number;
|
safeAreaTop: number;
|
||||||
setLogoLoadError: (error: boolean) => void;
|
setLogoLoadError: (error: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -40,37 +32,20 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
|
||||||
handleBack,
|
handleBack,
|
||||||
handleToggleLibrary,
|
handleToggleLibrary,
|
||||||
inLibrary,
|
inLibrary,
|
||||||
headerOpacity,
|
|
||||||
headerElementsY,
|
|
||||||
headerElementsOpacity,
|
|
||||||
safeAreaTop,
|
safeAreaTop,
|
||||||
setLogoLoadError,
|
setLogoLoadError,
|
||||||
}) => {
|
}) => {
|
||||||
const { currentTheme } = useTheme();
|
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 (
|
return (
|
||||||
<Animated.View style={[styles.floatingHeader, headerAnimatedStyle]}>
|
<View style={styles.floatingHeader}>
|
||||||
{Platform.OS === 'ios' ? (
|
{Platform.OS === 'ios' ? (
|
||||||
<ExpoBlurView
|
<ExpoBlurView
|
||||||
intensity={50}
|
intensity={50}
|
||||||
tint="dark"
|
tint="dark"
|
||||||
style={[styles.blurContainer, { paddingTop: Math.max(safeAreaTop * 0.8, safeAreaTop - 6) }]}
|
style={[styles.blurContainer, { paddingTop: Math.max(safeAreaTop * 0.8, safeAreaTop - 6) }]}
|
||||||
>
|
>
|
||||||
<Animated.View style={[styles.floatingHeaderContent, headerElementsStyle]}>
|
<View style={styles.floatingHeaderContent}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={handleBack}
|
onPress={handleBack}
|
||||||
|
|
@ -111,7 +86,7 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
|
||||||
color={currentTheme.colors.highEmphasis}
|
color={currentTheme.colors.highEmphasis}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Animated.View>
|
</View>
|
||||||
</ExpoBlurView>
|
</ExpoBlurView>
|
||||||
) : (
|
) : (
|
||||||
<View style={[styles.blurContainer, { paddingTop: Math.max(safeAreaTop * 0.8, safeAreaTop - 6) }]}>
|
<View style={[styles.blurContainer, { paddingTop: Math.max(safeAreaTop * 0.8, safeAreaTop - 6) }]}>
|
||||||
|
|
@ -121,7 +96,7 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
|
||||||
blurAmount={15}
|
blurAmount={15}
|
||||||
reducedTransparencyFallbackColor="rgba(20, 20, 20, 0.9)"
|
reducedTransparencyFallbackColor="rgba(20, 20, 20, 0.9)"
|
||||||
/>
|
/>
|
||||||
<Animated.View style={[styles.floatingHeaderContent, headerElementsStyle]}>
|
<View style={styles.floatingHeaderContent}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={handleBack}
|
onPress={handleBack}
|
||||||
|
|
@ -162,11 +137,11 @@ const FloatingHeader: React.FC<FloatingHeaderProps> = ({
|
||||||
color={currentTheme.colors.highEmphasis}
|
color={currentTheme.colors.highEmphasis}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Animated.View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{Platform.OS === 'ios' && <View style={[styles.headerBottomBorder, { backgroundColor: 'rgba(255,255,255,0.15)' }]} />}
|
{Platform.OS === 'ios' && <View style={[styles.headerBottomBorder, { backgroundColor: 'rgba(255,255,255,0.15)' }]} />}
|
||||||
</Animated.View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Dimensions } from 'react-native';
|
import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Dimensions } from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import Animated, {
|
// Removed react-native-reanimated imports
|
||||||
FadeIn,
|
|
||||||
FadeOut,
|
|
||||||
SlideInRight,
|
|
||||||
SlideOutRight,
|
|
||||||
} from 'react-native-reanimated';
|
|
||||||
import { styles } from '../utils/playerStyles';
|
import { styles } from '../utils/playerStyles';
|
||||||
import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes';
|
import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes';
|
||||||
import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils';
|
import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils';
|
||||||
|
|
@ -89,9 +84,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
<Animated.View
|
<View
|
||||||
entering={FadeIn.duration(200)}
|
|
||||||
exiting={FadeOut.duration(150)}
|
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
|
|
@ -107,12 +100,10 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
onPress={handleClose}
|
onPress={handleClose}
|
||||||
activeOpacity={1}
|
activeOpacity={1}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</View>
|
||||||
|
|
||||||
{/* Side Menu */}
|
{/* Side Menu */}
|
||||||
<Animated.View
|
<View
|
||||||
entering={SlideInRight.duration(300)}
|
|
||||||
exiting={SlideOutRight.duration(250)}
|
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
|
|
@ -527,7 +518,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</Animated.View>
|
</View>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -13,12 +13,13 @@ import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Platform,
|
Platform,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
|
TVEventHandler,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { NavigationProp } from '@react-navigation/native';
|
import { NavigationProp } from '@react-navigation/native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { Image } from 'expo-image';
|
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 { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { catalogService } from '../services/catalogService';
|
import { catalogService } from '../services/catalogService';
|
||||||
import type { StreamingContent } from '../services/catalogService';
|
import type { StreamingContent } from '../services/catalogService';
|
||||||
|
|
@ -203,7 +204,7 @@ const SkeletonLoader = () => {
|
||||||
const LibraryScreen = () => {
|
const LibraryScreen = () => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const isDarkMode = useColorScheme() === 'dark';
|
const isDarkMode = useColorScheme() === 'dark';
|
||||||
const { width } = useWindowDimensions();
|
const { width, height } = useWindowDimensions();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [libraryItems, setLibraryItems] = useState<LibraryItem[]>([]);
|
const [libraryItems, setLibraryItems] = useState<LibraryItem[]>([]);
|
||||||
const [filter, setFilter] = useState<'all' | 'movies' | 'series'>('all');
|
const [filter, setFilter] = useState<'all' | 'movies' | 'series'>('all');
|
||||||
|
|
@ -212,6 +213,22 @@ const LibraryScreen = () => {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { currentTheme } = useTheme();
|
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
|
// Trakt integration
|
||||||
const {
|
const {
|
||||||
isAuthenticated: traktAuthenticated,
|
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 => {
|
const filteredItems = libraryItems.filter(item => {
|
||||||
if (filter === 'all') return true;
|
if (filter === 'all') return true;
|
||||||
if (filter === 'movies') return item.type === 'movie';
|
if (filter === 'movies') return item.type === 'movie';
|
||||||
|
|
@ -328,15 +390,37 @@ const LibraryScreen = () => {
|
||||||
return folders.filter(folder => folder.itemCount > 0);
|
return folders.filter(folder => folder.itemCount > 0);
|
||||||
}, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]);
|
}, [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 }) => (
|
const renderItem = ({ item }: { item: LibraryItem }) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.itemContainer, { width: itemWidth }]}
|
style={[
|
||||||
|
styles.itemContainer,
|
||||||
|
{
|
||||||
|
width: itemWidth,
|
||||||
|
marginHorizontal: itemSpacing / 2,
|
||||||
|
}
|
||||||
|
]}
|
||||||
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
|
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
|
||||||
activeOpacity={0.7}
|
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
|
<Image
|
||||||
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
||||||
style={styles.poster}
|
style={styles.poster}
|
||||||
|
|
@ -348,13 +432,24 @@ const LibraryScreen = () => {
|
||||||
style={styles.posterGradient}
|
style={styles.posterGradient}
|
||||||
>
|
>
|
||||||
<Text
|
<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}
|
numberOfLines={2}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
{item.lastWatched && (
|
{item.lastWatched && (
|
||||||
<Text style={styles.lastWatched}>
|
<Text style={[
|
||||||
|
styles.lastWatched,
|
||||||
|
{
|
||||||
|
fontSize: width > 1920 ? 14 : width > 1280 ? 13 : 12, // Responsive font size
|
||||||
|
}
|
||||||
|
]}>
|
||||||
{item.lastWatched}
|
{item.lastWatched}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
@ -365,7 +460,11 @@ const LibraryScreen = () => {
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.progressBar,
|
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>
|
</View>
|
||||||
|
|
@ -374,11 +473,17 @@ const LibraryScreen = () => {
|
||||||
<View style={styles.badgeContainer}>
|
<View style={styles.badgeContainer}>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="live-tv"
|
name="live-tv"
|
||||||
size={14}
|
size={width > 1920 ? 18 : width > 1280 ? 16 : 14} // Responsive icon size
|
||||||
color={currentTheme.colors.white}
|
color={currentTheme.colors.white}
|
||||||
style={{ marginRight: 4 }}
|
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>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -388,31 +493,69 @@ const LibraryScreen = () => {
|
||||||
// Render individual Trakt collection folder
|
// Render individual Trakt collection folder
|
||||||
const renderTraktCollectionFolder = ({ folder }: { folder: TraktFolder }) => (
|
const renderTraktCollectionFolder = ({ folder }: { folder: TraktFolder }) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.itemContainer, { width: itemWidth }]}
|
style={[
|
||||||
|
styles.itemContainer,
|
||||||
|
{
|
||||||
|
width: itemWidth,
|
||||||
|
marginHorizontal: itemSpacing / 2,
|
||||||
|
}
|
||||||
|
]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSelectedTraktFolder(folder.id);
|
setSelectedTraktFolder(folder.id);
|
||||||
loadAllCollections(); // Load all collections when entering a specific folder
|
loadAllCollections(); // Load all collections when entering a specific folder
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
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
|
<LinearGradient
|
||||||
colors={folder.gradient}
|
colors={folder.gradient}
|
||||||
style={styles.folderGradient}
|
style={styles.folderGradient}
|
||||||
>
|
>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={folder.icon}
|
name={folder.icon}
|
||||||
size={60}
|
size={width > 1920 ? 80 : width > 1280 ? 70 : 60} // Responsive icon size
|
||||||
color={currentTheme.colors.white}
|
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}
|
{folder.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.folderCount}>
|
<Text style={[
|
||||||
|
styles.folderCount,
|
||||||
|
{
|
||||||
|
fontSize: width > 1920 ? 14 : width > 1280 ? 13 : 12, // Responsive font size
|
||||||
|
}
|
||||||
|
]}>
|
||||||
{folder.itemCount} items
|
{folder.itemCount} items
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.folderSubtitle}>
|
<Text style={[
|
||||||
|
styles.folderSubtitle,
|
||||||
|
{
|
||||||
|
fontSize: width > 1920 ? 14 : width > 1280 ? 13 : 12, // Responsive font size
|
||||||
|
}
|
||||||
|
]}>
|
||||||
{folder.description}
|
{folder.description}
|
||||||
</Text>
|
</Text>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
|
@ -859,14 +1002,28 @@ const LibraryScreen = () => {
|
||||||
return renderItem({ item: item as LibraryItem });
|
return renderItem({ item: item as LibraryItem });
|
||||||
}}
|
}}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
numColumns={2}
|
numColumns={numColumns}
|
||||||
contentContainerStyle={styles.listContainer}
|
contentContainerStyle={[
|
||||||
|
styles.listContainer,
|
||||||
|
{
|
||||||
|
paddingHorizontal: containerPadding,
|
||||||
|
paddingVertical: width > 1920 ? 24 : width > 1280 ? 20 : 16,
|
||||||
|
paddingBottom: width > 1920 ? 120 : 90,
|
||||||
|
}
|
||||||
|
]}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
columnWrapperStyle={styles.columnWrapper}
|
columnWrapperStyle={numColumns > 1 ? [
|
||||||
initialNumToRender={6}
|
styles.columnWrapper,
|
||||||
maxToRenderPerBatch={6}
|
{
|
||||||
|
marginBottom: width > 1920 ? 24 : width > 1280 ? 20 : 16,
|
||||||
|
}
|
||||||
|
] : undefined}
|
||||||
|
initialNumToRender={numColumns * 3}
|
||||||
|
maxToRenderPerBatch={numColumns * 2}
|
||||||
windowSize={5}
|
windowSize={5}
|
||||||
removeClippedSubviews={Platform.OS === 'android'}
|
removeClippedSubviews={Platform.OS === 'android'}
|
||||||
|
// TV optimizations
|
||||||
|
getItemLayout={undefined} // Let FlatList calculate for TV focus
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -1017,7 +1174,7 @@ const styles = StyleSheet.create({
|
||||||
paddingBottom: 90,
|
paddingBottom: 90,
|
||||||
},
|
},
|
||||||
columnWrapper: {
|
columnWrapper: {
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'flex-start',
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
skeletonContainer: {
|
skeletonContainer: {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
|
ScrollView,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useRoute, useNavigation } from '@react-navigation/native';
|
import { useRoute, useNavigation } from '@react-navigation/native';
|
||||||
|
|
@ -21,13 +22,6 @@ import { MovieContent } from '../components/metadata/MovieContent';
|
||||||
import { MoreLikeThisSection } from '../components/metadata/MoreLikeThisSection';
|
import { MoreLikeThisSection } from '../components/metadata/MoreLikeThisSection';
|
||||||
import { RatingsSection } from '../components/metadata/RatingsSection';
|
import { RatingsSection } from '../components/metadata/RatingsSection';
|
||||||
import { RouteParams, Episode } from '../types/metadata';
|
import { RouteParams, Episode } from '../types/metadata';
|
||||||
import Animated, {
|
|
||||||
useAnimatedStyle,
|
|
||||||
interpolate,
|
|
||||||
Extrapolate,
|
|
||||||
useSharedValue,
|
|
||||||
withTiming,
|
|
||||||
} from 'react-native-reanimated';
|
|
||||||
import { RouteProp } from '@react-navigation/native';
|
import { RouteProp } from '@react-navigation/native';
|
||||||
import { NavigationProp } from '@react-navigation/native';
|
import { NavigationProp } from '@react-navigation/native';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
|
|
@ -38,7 +32,6 @@ import { MetadataLoadingScreen } from '../components/loading/MetadataLoadingScre
|
||||||
import HeroSection from '../components/metadata/HeroSection';
|
import HeroSection from '../components/metadata/HeroSection';
|
||||||
import FloatingHeader from '../components/metadata/FloatingHeader';
|
import FloatingHeader from '../components/metadata/FloatingHeader';
|
||||||
import MetadataDetails from '../components/metadata/MetadataDetails';
|
import MetadataDetails from '../components/metadata/MetadataDetails';
|
||||||
import { useMetadataAnimations } from '../hooks/useMetadataAnimations';
|
|
||||||
import { useMetadataAssets } from '../hooks/useMetadataAssets';
|
import { useMetadataAssets } from '../hooks/useMetadataAssets';
|
||||||
import { useWatchProgress } from '../hooks/useWatchProgress';
|
import { useWatchProgress } from '../hooks/useWatchProgress';
|
||||||
import { TraktService, TraktPlaybackItem } from '../services/traktService';
|
import { TraktService, TraktPlaybackItem } from '../services/traktService';
|
||||||
|
|
@ -59,7 +52,7 @@ const MetadataScreen: React.FC = () => {
|
||||||
const [isContentReady, setIsContentReady] = useState(false);
|
const [isContentReady, setIsContentReady] = useState(false);
|
||||||
const [showCastModal, setShowCastModal] = useState(false);
|
const [showCastModal, setShowCastModal] = useState(false);
|
||||||
const [selectedCastMember, setSelectedCastMember] = useState<any>(null);
|
const [selectedCastMember, setSelectedCastMember] = useState<any>(null);
|
||||||
const transitionOpacity = useSharedValue(1);
|
// Removed animation state
|
||||||
|
|
||||||
const {
|
const {
|
||||||
metadata,
|
metadata,
|
||||||
|
|
@ -84,7 +77,6 @@ const MetadataScreen: React.FC = () => {
|
||||||
// Optimized hooks with memoization
|
// Optimized hooks with memoization
|
||||||
const watchProgressData = useWatchProgress(id, type as 'movie' | 'series', episodeId, episodes);
|
const watchProgressData = useWatchProgress(id, type as 'movie' | 'series', episodeId, episodes);
|
||||||
const assetData = useMetadataAssets(metadata, id, type, imdbId, settings, setMetadata);
|
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
|
// Fetch and log Trakt progress data when entering the screen
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -192,11 +184,10 @@ const MetadataScreen: React.FC = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isReady) {
|
if (isReady) {
|
||||||
setIsContentReady(true);
|
setIsContentReady(true);
|
||||||
transitionOpacity.value = withTiming(1, { duration: 50 });
|
// Removed animation logic
|
||||||
} else if (!isReady && isContentReady) {
|
} else if (!isReady && isContentReady) {
|
||||||
setIsContentReady(false);
|
setIsContentReady(false);
|
||||||
transitionOpacity.value = 0;
|
}
|
||||||
}
|
|
||||||
}, [isReady, isContentReady]);
|
}, [isReady, isContentReady]);
|
||||||
|
|
||||||
// Optimized callback functions with reduced dependencies
|
// Optimized callback functions with reduced dependencies
|
||||||
|
|
@ -318,19 +309,7 @@ const MetadataScreen: React.FC = () => {
|
||||||
setShowCastModal(true);
|
setShowCastModal(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Ultra-optimized animated styles - minimal calculations
|
// Removed animated styles
|
||||||
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,
|
|
||||||
}), []);
|
|
||||||
|
|
||||||
// Memoized error component for performance
|
// Memoized error component for performance
|
||||||
const ErrorComponent = useMemo(() => {
|
const ErrorComponent = useMemo(() => {
|
||||||
|
|
@ -377,7 +356,7 @@ const MetadataScreen: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
style={[containerStyle, styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}
|
style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}
|
||||||
edges={['bottom']}
|
edges={['bottom']}
|
||||||
>
|
>
|
||||||
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" animated />
|
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" animated />
|
||||||
|
|
@ -390,19 +369,14 @@ const MetadataScreen: React.FC = () => {
|
||||||
logoLoadError={assetData.logoLoadError}
|
logoLoadError={assetData.logoLoadError}
|
||||||
handleBack={handleBack}
|
handleBack={handleBack}
|
||||||
handleToggleLibrary={handleToggleLibrary}
|
handleToggleLibrary={handleToggleLibrary}
|
||||||
headerElementsY={animations.headerElementsY}
|
|
||||||
inLibrary={inLibrary}
|
inLibrary={inLibrary}
|
||||||
headerOpacity={animations.headerOpacity}
|
|
||||||
headerElementsOpacity={animations.headerElementsOpacity}
|
|
||||||
safeAreaTop={safeAreaTop}
|
safeAreaTop={safeAreaTop}
|
||||||
setLogoLoadError={assetData.setLogoLoadError}
|
setLogoLoadError={assetData.setLogoLoadError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Animated.ScrollView
|
<ScrollView
|
||||||
style={styles.scrollView}
|
style={styles.scrollView}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
onScroll={animations.scrollHandler}
|
|
||||||
scrollEventThrottle={16}
|
|
||||||
bounces={false}
|
bounces={false}
|
||||||
overScrollMode="never"
|
overScrollMode="never"
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
|
@ -413,14 +387,6 @@ const MetadataScreen: React.FC = () => {
|
||||||
bannerImage={assetData.bannerImage}
|
bannerImage={assetData.bannerImage}
|
||||||
loadingBanner={assetData.loadingBanner}
|
loadingBanner={assetData.loadingBanner}
|
||||||
logoLoadError={assetData.logoLoadError}
|
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}
|
watchProgress={watchProgressData.watchProgress}
|
||||||
type={type as 'movie' | 'series'}
|
type={type as 'movie' | 'series'}
|
||||||
getEpisodeDetails={watchProgressData.getEpisodeDetails}
|
getEpisodeDetails={watchProgressData.getEpisodeDetails}
|
||||||
|
|
@ -436,7 +402,7 @@ const MetadataScreen: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Main Content - Optimized */}
|
{/* Main Content - Optimized */}
|
||||||
<Animated.View style={contentStyle}>
|
<View>
|
||||||
<MetadataDetails
|
<MetadataDetails
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
imdbId={imdbId}
|
imdbId={imdbId}
|
||||||
|
|
@ -475,8 +441,8 @@ const MetadataScreen: React.FC = () => {
|
||||||
) : (
|
) : (
|
||||||
metadata && <MovieContent metadata={metadata} />
|
metadata && <MovieContent metadata={metadata} />
|
||||||
)}
|
)}
|
||||||
</Animated.View>
|
</View>
|
||||||
</Animated.ScrollView>
|
</ScrollView>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,7 @@ import {
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import Animated, {
|
// Removed react-native-reanimated imports
|
||||||
useSharedValue,
|
|
||||||
useAnimatedStyle,
|
|
||||||
withSpring,
|
|
||||||
withTiming,
|
|
||||||
FadeInDown,
|
|
||||||
FadeInUp,
|
|
||||||
} from 'react-native-reanimated';
|
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
|
|
@ -77,18 +70,14 @@ const OnboardingScreen = () => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const flatListRef = useRef<FlatList>(null);
|
const flatListRef = useRef<FlatList>(null);
|
||||||
const progressValue = useSharedValue(0);
|
// Removed animated progress values
|
||||||
|
|
||||||
const animatedProgressStyle = useAnimatedStyle(() => ({
|
|
||||||
width: withSpring(`${((currentIndex + 1) / onboardingData.length) * 100}%`),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
if (currentIndex < onboardingData.length - 1) {
|
if (currentIndex < onboardingData.length - 1) {
|
||||||
const nextIndex = currentIndex + 1;
|
const nextIndex = currentIndex + 1;
|
||||||
setCurrentIndex(nextIndex);
|
setCurrentIndex(nextIndex);
|
||||||
flatListRef.current?.scrollToIndex({ index: nextIndex, animated: true });
|
flatListRef.current?.scrollToIndex({ index: nextIndex, animated: true });
|
||||||
progressValue.value = (nextIndex + 1) / onboardingData.length;
|
// Removed progress animation
|
||||||
} else {
|
} else {
|
||||||
handleGetStarted();
|
handleGetStarted();
|
||||||
}
|
}
|
||||||
|
|
@ -125,22 +114,16 @@ const OnboardingScreen = () => {
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 1 }}
|
end={{ x: 1, y: 1 }}
|
||||||
>
|
>
|
||||||
<Animated.View
|
<View style={styles.iconWrapper}>
|
||||||
entering={FadeInDown.delay(300).duration(800)}
|
|
||||||
style={styles.iconWrapper}
|
|
||||||
>
|
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={item.icon}
|
name={item.icon}
|
||||||
size={80}
|
size={80}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</View>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
|
||||||
<Animated.View
|
<View style={styles.textContainer}>
|
||||||
entering={FadeInUp.delay(500).duration(800)}
|
|
||||||
style={styles.textContainer}
|
|
||||||
>
|
|
||||||
<Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]}>
|
<Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -150,7 +133,7 @@ const OnboardingScreen = () => {
|
||||||
<Text style={[styles.description, { color: currentTheme.colors.mediumEmphasis }]}>
|
<Text style={[styles.description, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
{item.description}
|
{item.description}
|
||||||
</Text>
|
</Text>
|
||||||
</Animated.View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -188,11 +171,10 @@ const OnboardingScreen = () => {
|
||||||
|
|
||||||
{/* Progress Bar */}
|
{/* Progress Bar */}
|
||||||
<View style={[styles.progressContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
<View style={[styles.progressContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||||
<Animated.View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.progressBar,
|
styles.progressBar,
|
||||||
{ backgroundColor: currentTheme.colors.primary },
|
{ backgroundColor: currentTheme.colors.primary, width: `${((currentIndex + 1) / onboardingData.length) * 100}%` }
|
||||||
animatedProgressStyle
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue