import React, { useState, useEffect, useCallback, useRef } from 'react';
import {
View,
Text,
StyleSheet,
FlatList,
TouchableOpacity,
ActivityIndicator,
RefreshControl,
SafeAreaView,
StatusBar,
useColorScheme,
Dimensions,
ImageBackground,
ScrollView,
Platform,
Image,
Modal,
Pressable
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator';
import { StreamingContent, CatalogContent, catalogService } from '../services/catalogService';
import { Stream } from '../types/metadata';
import { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { Image as ExpoImage } from 'expo-image';
import { colors } from '../styles/colors';
import Animated, {
FadeIn,
FadeOut,
useAnimatedStyle,
withSpring,
withTiming,
useSharedValue,
interpolate,
Extrapolate,
runOnJS,
useAnimatedGestureHandler,
} from 'react-native-reanimated';
import { PanGestureHandler } from 'react-native-gesture-handler';
import {
Gesture,
GestureDetector,
GestureHandlerRootView,
} from 'react-native-gesture-handler';
import { useCatalogContext } from '../contexts/CatalogContext';
import { ThisWeekSection } from '../components/home/ThisWeekSection';
import ContinueWatchingSection from '../components/home/ContinueWatchingSection';
import * as Haptics from 'expo-haptics';
import { tmdbService } from '../services/tmdbService';
import { logger } from '../utils/logger';
import { storageService } from '../services/storageService';
import { useHomeCatalogs } from '../hooks/useHomeCatalogs';
import { useFeaturedContent } from '../hooks/useFeaturedContent';
import { useSettings, settingsEmitter } from '../hooks/useSettings';
// Define interfaces for our data
interface Category {
id: string;
name: string;
}
interface ContentItemProps {
item: StreamingContent;
onPress: (id: string, type: string) => void;
}
interface DropUpMenuProps {
visible: boolean;
onClose: () => void;
item: StreamingContent;
onOptionSelect: (option: string) => void;
}
const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) => {
const translateY = useSharedValue(300);
const opacity = useSharedValue(0);
const isDarkMode = useColorScheme() === 'dark';
const SNAP_THRESHOLD = 100;
useEffect(() => {
if (visible) {
opacity.value = withTiming(1, { duration: 200 });
translateY.value = withTiming(0, { duration: 300 });
} else {
opacity.value = withTiming(0, { duration: 200 });
translateY.value = withTiming(300, { duration: 300 });
}
}, [visible]);
const gesture = Gesture.Pan()
.onStart(() => {
// Store initial position if needed
})
.onUpdate((event) => {
if (event.translationY > 0) { // Only allow dragging downwards
translateY.value = event.translationY;
opacity.value = interpolate(
event.translationY,
[0, 300],
[1, 0],
Extrapolate.CLAMP
);
}
})
.onEnd((event) => {
if (event.translationY > SNAP_THRESHOLD || event.velocityY > 500) {
translateY.value = withTiming(300, { duration: 300 });
opacity.value = withTiming(0, { duration: 200 });
runOnJS(onClose)();
} else {
translateY.value = withTiming(0, { duration: 300 });
opacity.value = withTiming(1, { duration: 200 });
}
});
const overlayStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
}));
const menuStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
}));
const menuOptions = [
{
icon: item.inLibrary ? 'bookmark' : 'bookmark-border',
label: item.inLibrary ? 'Remove from Library' : 'Add to Library',
action: 'library'
},
{
icon: 'check-circle',
label: 'Mark as Watched',
action: 'watched'
},
{
icon: 'playlist-add',
label: 'Add to Playlist',
action: 'playlist'
},
{
icon: 'share',
label: 'Share',
action: 'share'
}
];
const backgroundColor = isDarkMode ? '#1A1A1A' : '#FFFFFF';
return (
{item.name}
{item.year && (
{item.year}
)}
{menuOptions.map((option, index) => (
{
onOptionSelect(option.action);
onClose();
}}
>
{option.label}
))}
);
};
const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
const [menuVisible, setMenuVisible] = useState(false);
const [localItem, setLocalItem] = useState(initialItem);
const [isWatched, setIsWatched] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
const [imageError, setImageError] = useState(false);
const handleLongPress = useCallback(() => {
setMenuVisible(true);
}, []);
const handlePress = useCallback(() => {
onPress(localItem.id, localItem.type);
}, [localItem.id, localItem.type, onPress]);
const handleOptionSelect = useCallback((option: string) => {
switch (option) {
case 'library':
if (localItem.inLibrary) {
catalogService.removeFromLibrary(localItem.type, localItem.id);
} else {
catalogService.addToLibrary(localItem);
}
break;
case 'watched':
setIsWatched(prev => !prev);
break;
case 'playlist':
break;
case 'share':
break;
}
}, [localItem]);
const handleMenuClose = useCallback(() => {
setMenuVisible(false);
}, []);
useEffect(() => {
setLocalItem(initialItem);
}, [initialItem]);
useEffect(() => {
const unsubscribe = catalogService.subscribeToLibraryUpdates((libraryItems) => {
const isInLibrary = libraryItems.some(
libraryItem => libraryItem.id === localItem.id && libraryItem.type === localItem.type
);
setLocalItem(prev => ({ ...prev, inLibrary: isInLibrary }));
});
return () => unsubscribe();
}, [localItem.id, localItem.type]);
return (
<>
{
setImageLoaded(false);
setImageError(false);
}}
onLoadEnd={() => setImageLoaded(true)}
onError={() => {
setImageError(true);
setImageLoaded(true);
}}
/>
{(!imageLoaded || imageError) && (
{!imageError ? (
) : (
)}
)}
{isWatched && (
)}
{localItem.inLibrary && (
)}
>
);
};
// Sample categories (real app would get these from API)
const SAMPLE_CATEGORIES: Category[] = [
{ id: 'movie', name: 'Movies' },
{ id: 'series', name: 'Series' },
{ id: 'channel', name: 'Channels' },
];
const SkeletonCatalog = () => (
);
const SkeletonFeatured = () => (
Loading featured content...
);
const HomeScreen = () => {
const navigation = useNavigation>();
const isDarkMode = useColorScheme() === 'dark';
const continueWatchingRef = useRef<{ refresh: () => Promise }>(null);
const { settings } = useSettings();
const [showHeroSection, setShowHeroSection] = useState(settings.showHeroSection);
const [featuredContentSource, setFeaturedContentSource] = useState(settings.featuredContentSource);
const refreshTimeoutRef = useRef(null);
const {
catalogs,
loading: catalogsLoading,
refreshing: catalogsRefreshing,
refreshCatalogs
} = useHomeCatalogs();
const {
featuredContent,
loading: featuredLoading,
isSaved,
handleSaveToLibrary,
refreshFeatured
} = useFeaturedContent();
// Only count feature section as loading if it's enabled in settings
const isLoading = (showHeroSection ? featuredLoading : false) || catalogsLoading;
const isRefreshing = catalogsRefreshing;
// React to settings changes
useEffect(() => {
setShowHeroSection(settings.showHeroSection);
setFeaturedContentSource(settings.featuredContentSource);
}, [settings]);
// If featured content source changes, refresh featured content with debouncing
useEffect(() => {
if (showHeroSection) {
// Clear any existing timeout
if (refreshTimeoutRef.current) {
clearTimeout(refreshTimeoutRef.current);
}
// Set a new timeout to debounce the refresh
refreshTimeoutRef.current = setTimeout(() => {
refreshFeatured();
refreshTimeoutRef.current = null;
}, 300);
}
// Cleanup the timeout on unmount
return () => {
if (refreshTimeoutRef.current) {
clearTimeout(refreshTimeoutRef.current);
}
};
}, [featuredContentSource, showHeroSection, refreshFeatured]);
useEffect(() => {
StatusBar.setTranslucent(true);
StatusBar.setBackgroundColor('transparent');
return () => {
StatusBar.setTranslucent(false);
StatusBar.setBackgroundColor(colors.darkBackground);
};
}, []);
useEffect(() => {
navigation.addListener('beforeRemove', () => {});
return () => {
navigation.removeListener('beforeRemove', () => {});
};
}, [navigation]);
const preloadImages = useCallback(async (content: StreamingContent[]) => {
if (!content.length) return;
try {
const imagePromises = content.map(item => {
const imagesToLoad = [
item.poster,
item.banner,
item.logo
].filter(Boolean) as string[];
return Promise.all(
imagesToLoad.map(imageUrl =>
ExpoImage.prefetch(imageUrl)
)
);
});
await Promise.all(imagePromises);
} catch (error) {
console.error('Error preloading images:', error);
}
}, []);
const handleRefresh = useCallback(async () => {
try {
const refreshTasks = [
refreshCatalogs(),
continueWatchingRef.current?.refresh(),
];
// Only refresh featured content if hero section is enabled
if (showHeroSection) {
refreshTasks.push(refreshFeatured());
}
await Promise.all(refreshTasks);
} catch (error) {
logger.error('Error during refresh:', error);
}
}, [refreshFeatured, refreshCatalogs, showHeroSection]);
const handleContentPress = useCallback((id: string, type: string) => {
navigation.navigate('Metadata', { id, type });
}, [navigation]);
const handlePlayStream = useCallback((stream: Stream) => {
if (!featuredContent) return;
navigation.navigate('Player', {
uri: stream.url,
title: featuredContent.name,
year: featuredContent.year,
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
streamProvider: stream.name,
id: featuredContent.id,
type: featuredContent.type
});
}, [featuredContent, navigation]);
const refreshContinueWatching = useCallback(() => {
if (continueWatchingRef.current) {
continueWatchingRef.current.refresh();
}
}, []);
useEffect(() => {
const handlePlaybackComplete = () => {
refreshContinueWatching();
};
const unsubscribe = navigation.addListener('focus', () => {
refreshContinueWatching();
});
return () => {
unsubscribe();
};
}, [navigation, refreshContinueWatching]);
const renderFeaturedContent = () => {
if (!featuredContent) {
return ;
}
return (
{
if (featuredContent) {
navigation.navigate('Metadata', {
id: featuredContent.id,
type: featuredContent.type
});
}
}}
style={styles.featuredContainer}
>
{featuredContent.logo ? (
) : (
{featuredContent.name}
)}
{featuredContent.genres?.slice(0, 3).map((genre, index, array) => (
{genre}
{index < array.length - 1 && (
•
)}
))}
{isSaved ? "Saved" : "Save"}
{
if (featuredContent) {
navigation.navigate('Streams', {
id: featuredContent.id,
type: featuredContent.type
});
}
}}
>
Play
{
if (featuredContent) {
navigation.navigate('Metadata', {
id: featuredContent.id,
type: featuredContent.type
});
}
}}
>
Info
);
};
const renderContentItem = useCallback(({ item, index }: { item: StreamingContent, index: number }) => {
return (
);
}, [handleContentPress]);
const renderCatalog = ({ item }: { item: CatalogContent }) => {
return (
{item.name}
navigation.navigate('Catalog', {
id: item.id,
type: item.type,
addonId: item.addon
})
}
style={styles.seeAllButton}
>
See More
renderContentItem({ item, index })}
keyExtractor={(item) => `${item.id}-${item.type}`}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.catalogList}
snapToInterval={POSTER_WIDTH + 12}
decelerationRate="fast"
snapToAlignment="start"
ItemSeparatorComponent={() => }
initialNumToRender={4}
maxToRenderPerBatch={4}
windowSize={5}
removeClippedSubviews={Platform.OS === 'android'}
getItemLayout={(data, index) => ({
length: POSTER_WIDTH + 12,
offset: (POSTER_WIDTH + 12) * index,
index,
})}
/>
);
};
if (isLoading && !isRefreshing) {
return (
Loading your content...
);
}
return (
}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{showHeroSection && renderFeaturedContent()}
{catalogs.length > 0 ? (
catalogs.map((catalog, index) => (
{renderCatalog({ item: catalog })}
))
) : (
!catalogsLoading && (
No content available
navigation.navigate('Settings')}
>
Add Catalogs
)
)}
);
};
const { width, height } = Dimensions.get('window');
const POSTER_WIDTH = (width - 50) / 3;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.darkBackground,
},
scrollContent: {
paddingBottom: 40,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
featuredContainer: {
width: '100%',
height: height * 0.6,
marginTop: Platform.OS === 'ios' ? 85 : 75,
marginBottom: 8,
position: 'relative',
},
featuredBanner: {
width: '100%',
height: '100%',
},
featuredGradient: {
width: '100%',
height: '100%',
justifyContent: 'space-between',
},
featuredContent: {
padding: 24,
paddingBottom: 16,
alignItems: 'center',
flex: 1,
justifyContent: 'flex-end',
gap: 12,
},
featuredLogo: {
width: width * 0.7,
height: 100,
marginBottom: 0,
alignSelf: 'center',
},
featuredTitle: {
color: colors.white,
fontSize: 32,
fontWeight: '900',
marginBottom: 0,
textAlign: 'center',
textShadowColor: 'rgba(0, 0, 0, 0.5)',
textShadowOffset: { width: 0, height: 2 },
textShadowRadius: 4,
},
genreContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
flexWrap: 'wrap',
gap: 4,
},
genreText: {
color: colors.white,
fontSize: 14,
fontWeight: '500',
opacity: 0.9,
},
genreDot: {
color: colors.white,
fontSize: 14,
fontWeight: '500',
opacity: 0.6,
marginHorizontal: 4,
},
featuredButtons: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-evenly',
width: '100%',
flex: 1,
maxHeight: 65,
paddingTop: 16,
},
playButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 14,
paddingHorizontal: 32,
borderRadius: 30,
backgroundColor: colors.white,
elevation: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
flex: 0,
width: 150,
},
myListButton: {
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
padding: 0,
gap: 6,
width: 44,
height: 44,
flex: null,
},
infoButton: {
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
padding: 0,
gap: 4,
width: 44,
height: 44,
flex: null,
},
playButtonText: {
color: colors.black,
fontWeight: '600',
marginLeft: 8,
fontSize: 16,
},
myListButtonText: {
color: colors.white,
fontSize: 12,
fontWeight: '500',
},
infoButtonText: {
color: colors.white,
fontSize: 12,
fontWeight: '500',
},
catalogContainer: {
marginBottom: 24,
paddingTop: 0,
marginTop: 16,
},
catalogHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
marginBottom: 12,
},
titleContainer: {
position: 'relative',
},
catalogTitle: {
fontSize: 18,
fontWeight: '800',
color: colors.highEmphasis,
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 6,
},
titleUnderline: {
position: 'absolute',
bottom: -4,
left: 0,
width: 60,
height: 3,
borderRadius: 1.5,
},
seeAllButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.elevation1,
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
},
seeAllText: {
color: colors.primary,
fontSize: 13,
fontWeight: '700',
marginRight: 4,
},
catalogList: {
paddingHorizontal: 16,
paddingBottom: 12,
paddingTop: 6,
},
contentItem: {
width: POSTER_WIDTH,
aspectRatio: 2/3,
margin: 0,
borderRadius: 16,
overflow: 'hidden',
position: 'relative',
elevation: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.08)',
},
poster: {
width: '100%',
height: '100%',
borderRadius: 16,
},
imdbLogo: {
width: 35,
height: 17,
marginRight: 4,
},
ratingBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.75)',
paddingHorizontal: 6,
paddingVertical: 3,
borderRadius: 4,
},
ratingBadgeText: {
color: '#FFFFFF',
fontSize: 11,
fontWeight: 'bold',
marginLeft: 3,
},
emptyCatalog: {
padding: 32,
alignItems: 'center',
backgroundColor: colors.elevation1,
margin: 16,
borderRadius: 16,
},
skeletonBox: {
backgroundColor: colors.elevation2,
borderRadius: 16,
overflow: 'hidden',
},
skeletonFeatured: {
width: '100%',
height: height * 0.6,
backgroundColor: colors.elevation2,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
marginBottom: 0,
},
skeletonPoster: {
backgroundColor: colors.elevation1,
marginHorizontal: 4,
borderRadius: 16,
},
contentItemContainer: {
width: '100%',
height: '100%',
borderRadius: 16,
overflow: 'hidden',
position: 'relative',
},
libraryIndicatorContainer: {
alignItems: 'center',
justifyContent: 'center',
padding: 16,
},
libraryText: {
color: '#FFFFFF',
fontSize: 12,
fontWeight: '600',
marginTop: 8,
textAlign: 'center',
},
modalOverlay: {
flex: 1,
justifyContent: 'flex-end',
backgroundColor: colors.transparentDark,
},
modalOverlayPressable: {
flex: 1,
},
dragHandle: {
width: 40,
height: 4,
backgroundColor: colors.transparentLight,
borderRadius: 2,
alignSelf: 'center',
marginTop: 12,
marginBottom: 10,
},
menuContainer: {
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
paddingBottom: Platform.select({ ios: 40, android: 24 }),
...Platform.select({
ios: {
shadowColor: colors.black,
shadowOffset: { width: 0, height: -3 },
shadowOpacity: 0.1,
shadowRadius: 5,
},
android: {
elevation: 5,
},
}),
},
menuHeader: {
flexDirection: 'row',
padding: 16,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
},
menuPoster: {
width: 60,
height: 90,
borderRadius: 12,
},
menuTitleContainer: {
flex: 1,
marginLeft: 12,
justifyContent: 'center',
},
menuTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
menuYear: {
fontSize: 14,
},
menuOptions: {
paddingTop: 8,
},
menuOption: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
borderBottomWidth: StyleSheet.hairlineWidth,
},
lastMenuOption: {
borderBottomWidth: 0,
},
menuOptionText: {
fontSize: 16,
marginLeft: 16,
},
watchedIndicator: {
position: 'absolute',
top: 8,
right: 8,
backgroundColor: colors.transparentDark,
borderRadius: 12,
padding: 2,
},
libraryBadge: {
position: 'absolute',
top: 8,
left: 8,
backgroundColor: colors.transparentDark,
borderRadius: 8,
padding: 4,
},
loadingOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 16,
},
featuredImage: {
width: '100%',
height: '100%',
},
featuredContentContainer: {
flex: 1,
justifyContent: 'flex-end',
paddingHorizontal: 16,
paddingBottom: 20,
},
featuredTitleText: {
color: colors.highEmphasis,
fontSize: 28,
fontWeight: '900',
marginBottom: 8,
textShadowColor: 'rgba(0,0,0,0.6)',
textShadowOffset: { width: 0, height: 2 },
textShadowRadius: 4,
textAlign: 'center',
paddingHorizontal: 16,
},
addCatalogButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primary,
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 30,
marginTop: 16,
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 3,
},
addCatalogButtonText: {
color: colors.white,
fontSize: 14,
fontWeight: '600',
marginLeft: 8,
},
loadingMainContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 40,
},
loadingText: {
color: colors.textMuted,
marginTop: 12,
fontSize: 14,
},
loadingPlaceholder: {
height: 200,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.elevation1,
borderRadius: 12,
marginHorizontal: 16,
},
featuredLoadingContainer: {
height: height * 0.4,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.elevation1,
},
});
export default HomeScreen;