import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { View, Text, StyleSheet, TextInput, FlatList, TouchableOpacity, ActivityIndicator, useColorScheme, SafeAreaView, StatusBar, Keyboard, Dimensions, ScrollView, Animated as RNAnimated, Pressable, Platform, Easing, Modal, } from 'react-native'; import { useNavigation, useRoute, useFocusEffect } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { MaterialIcons, Feather } from '@expo/vector-icons'; import { catalogService, StreamingContent, GroupedSearchResults, AddonSearchResults } from '../services/catalogService'; import FastImage from '@d11/react-native-fast-image'; import debounce from 'lodash/debounce'; import { DropUpMenu } from '../components/home/DropUpMenu'; import { DeviceEventEmitter, Share } from 'react-native'; import { mmkvStorage } from '../services/mmkvStorage'; import Animated, { FadeIn, FadeOut, useAnimatedStyle, useSharedValue, withTiming, interpolate, withSpring, withDelay, } from 'react-native-reanimated'; import { RootStackParamList } from '../navigation/AppNavigator'; import { logger } from '../utils/logger'; import { BlurView } from 'expo-blur'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../contexts/ThemeContext'; import LoadingSpinner from '../components/common/LoadingSpinner'; import ScreenHeader from '../components/common/ScreenHeader'; import { useScrollToTop } from '../contexts/ScrollToTopContext'; import { BottomSheetModal, BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet'; import { useSettings } from '../hooks/useSettings'; // Import extracted search components import { DiscoverCatalog, BREAKPOINTS, getDeviceType, isTablet, isLargeTablet, isTV, TAB_BAR_HEIGHT, RECENT_SEARCHES_KEY, MAX_RECENT_SEARCHES, PLACEHOLDER_POSTER, HORIZONTAL_ITEM_WIDTH, HORIZONTAL_POSTER_HEIGHT, POSTER_WIDTH, POSTER_HEIGHT, } from '../components/search/searchUtils'; import { SearchSkeletonLoader } from '../components/search/SearchSkeletonLoader'; import { SearchAnimation } from '../components/search/SearchAnimation'; import { SearchResultItem } from '../components/search/SearchResultItem'; import { RecentSearches } from '../components/search/RecentSearches'; const { width, height } = Dimensions.get('window'); // Re-export for local use (backward compatibility) const deviceType = getDeviceType(width); const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity); // Alias imported components for backward compatibility with existing code const SkeletonLoader = SearchSkeletonLoader; const SimpleSearchAnimation = SearchAnimation; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; const SearchScreen = () => { const { settings } = useSettings(); const navigation = useNavigation>(); const isDarkMode = true; const [query, setQuery] = useState(''); const [results, setResults] = useState({ byAddon: [], allResults: [] }); const [searching, setSearching] = useState(false); const [searched, setSearched] = useState(false); const [recentSearches, setRecentSearches] = useState([]); const [showRecent, setShowRecent] = useState(true); const inputRef = useRef(null); const insets = useSafeAreaInsets(); const { currentTheme } = useTheme(); // Live search handle const liveSearchHandle = useRef<{ cancel: () => void; done: Promise } | null>(null); // Addon installation order map for stable section ordering const addonOrderRankRef = useRef>({}); // Track if this is the initial mount to prevent unnecessary operations const isInitialMount = useRef(true); // Track mount status for async operations const isMounted = useRef(true); const scrollViewRef = useRef(null); // Discover section state const [discoverCatalogs, setDiscoverCatalogs] = useState([]); const [selectedCatalog, setSelectedCatalog] = useState(null); const [selectedDiscoverType, setSelectedDiscoverType] = useState<'movie' | 'series'>('movie'); const [selectedDiscoverGenre, setSelectedDiscoverGenre] = useState(null); // Discover pagination state const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [discoverResults, setDiscoverResults] = useState([]); const [pendingDiscoverResults, setPendingDiscoverResults] = useState([]); const [discoverLoading, setDiscoverLoading] = useState(false); const [discoverInitialized, setDiscoverInitialized] = useState(false); // Bottom sheet refs and state const typeSheetRef = useRef(null); const catalogSheetRef = useRef(null); const genreSheetRef = useRef(null); const typeSnapPoints = useMemo(() => ['25%'], []); const catalogSnapPoints = useMemo(() => ['50%'], []); const genreSnapPoints = useMemo(() => ['50%'], []); // Scroll to top handler const scrollToTop = useCallback(() => { scrollViewRef.current?.scrollTo({ y: 0, animated: true }); }, []); useScrollToTop('Search', scrollToTop); useEffect(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, []); const handleShowMore = () => { if (pendingDiscoverResults.length === 0) return; // Show next batch of 300 items const batchSize = 300; const nextBatch = pendingDiscoverResults.slice(0, batchSize); const remaining = pendingDiscoverResults.slice(batchSize); setDiscoverResults(prev => [...prev, ...nextBatch]); setPendingDiscoverResults(remaining); }; // Load discover catalogs on mount useEffect(() => { const loadDiscoverCatalogs = async () => { try { const filters = await catalogService.getDiscoverFilters(); if (isMounted.current) { // Flatten catalogs from all types into a single array const allCatalogs: DiscoverCatalog[] = []; for (const [type, catalogs] of Object.entries(filters.catalogsByType)) { // Only include movie and series types if (type === 'movie' || type === 'series') { for (const catalog of catalogs) { allCatalogs.push({ ...catalog, type, }); } } } setDiscoverCatalogs(allCatalogs); // Auto-select first catalog if available if (allCatalogs.length > 0) { setSelectedCatalog(allCatalogs[0]); } setDiscoverInitialized(true); } } catch (error) { logger.error('Failed to load discover catalogs:', error); if (isMounted.current) { setDiscoverInitialized(true); } } }; loadDiscoverCatalogs(); }, []); // Fetch discover content when catalog or genre changes useEffect(() => { if (!discoverInitialized || !selectedCatalog || query.trim().length > 0) return; const fetchDiscoverContent = async () => { if (!isMounted.current) return; setDiscoverLoading(true); setPage(1); // Reset page on new filter setHasMore(true); setPendingDiscoverResults([]); try { const results = await catalogService.discoverContentFromCatalog( selectedCatalog.addonId, selectedCatalog.catalogId, selectedCatalog.type, selectedDiscoverGenre || undefined, 1 // page 1 ); if (isMounted.current) { if (results.length > 300) { setDiscoverResults(results.slice(0, 300)); setPendingDiscoverResults(results.slice(300)); setHasMore(true); } else { setDiscoverResults(results); setPendingDiscoverResults([]); setHasMore(results.length > 0); } } } catch (error) { logger.error('Failed to fetch discover content:', error); if (isMounted.current) { setDiscoverResults([]); } } finally { if (isMounted.current) { setDiscoverLoading(false); } } }; fetchDiscoverContent(); }, [discoverInitialized, selectedCatalog, selectedDiscoverGenre, query]); // Load more content for pagination const loadMoreDiscoverContent = async () => { if (!hasMore || loadingMore || discoverLoading || !selectedCatalog || pendingDiscoverResults.length > 0) return; setLoadingMore(true); const nextPage = page + 1; try { const moreResults = await catalogService.discoverContentFromCatalog( selectedCatalog.addonId, selectedCatalog.catalogId, selectedCatalog.type, selectedDiscoverGenre || undefined, nextPage ); if (isMounted.current) { if (moreResults.length > 0) { if (moreResults.length > 300) { setDiscoverResults(prev => [...prev, ...moreResults.slice(0, 300)]); setPendingDiscoverResults(moreResults.slice(300)); } else { setDiscoverResults(prev => [...prev, ...moreResults]); } setPage(nextPage); } else { setHasMore(false); } } } catch (error) { logger.error('Failed to load more discover content:', error); } finally { if (isMounted.current) { setLoadingMore(false); } } }; // DropUpMenu state const [menuVisible, setMenuVisible] = useState(false); const [selectedItem, setSelectedItem] = useState(null); const [isSaved, setIsSaved] = useState(false); const [isWatched, setIsWatched] = useState(false); const [refreshFlag, setRefreshFlag] = React.useState(false); // Update isSaved and isWatched when selectedItem changes useEffect(() => { if (!selectedItem) return; (async () => { // Check if item is in library const items = await catalogService.getLibraryItems(); const found = items.find((libItem: any) => libItem.id === selectedItem.id && libItem.type === selectedItem.type); setIsSaved(!!found); // Check watched status const val = await mmkvStorage.getItem(`watched:${selectedItem.type}:${selectedItem.id}`); setIsWatched(val === 'true'); })(); }, [selectedItem]); // Animation values const searchBarWidth = useSharedValue(width - 32); const searchBarOpacity = useSharedValue(1); const backButtonOpacity = useSharedValue(0); // Force consistent status bar settings useEffect(() => { const applyStatusBarConfig = () => { StatusBar.setBarStyle('light-content'); if (Platform.OS === 'android') { StatusBar.setTranslucent(true); StatusBar.setBackgroundColor('transparent'); } }; applyStatusBarConfig(); // Re-apply on focus const unsubscribe = navigation.addListener('focus', applyStatusBarConfig); return unsubscribe; }, [navigation]); React.useLayoutEffect(() => { navigation.setOptions({ headerShown: false, }); }, [navigation]); useEffect(() => { loadRecentSearches(); // Cleanup function to cancel pending searches on unmount return () => { debouncedSearch.cancel(); }; }, []); const animatedSearchBarStyle = useAnimatedStyle(() => { return { width: searchBarWidth.value, opacity: searchBarOpacity.value, }; }); const animatedBackButtonStyle = useAnimatedStyle(() => { return { opacity: backButtonOpacity.value, transform: [ { translateX: interpolate( backButtonOpacity.value, [0, 1], [-20, 0] ) } ] }; }); const handleSearchFocus = () => { // Animate search bar when focused searchBarWidth.value = withTiming(width - 80); backButtonOpacity.value = withTiming(1); }; const handleSearchBlur = () => { if (!query) { // Only animate back if query is empty searchBarWidth.value = withTiming(width - 32); backButtonOpacity.value = withTiming(0); } }; const handleBackPress = () => { Keyboard.dismiss(); if (query) { setQuery(''); setResults({ byAddon: [], allResults: [] }); setSearched(false); setShowRecent(true); loadRecentSearches(); } else { // Add a small delay to allow keyboard to dismiss smoothly before navigation if (Platform.OS === 'android') { setTimeout(() => { navigation.goBack(); }, 100); } else { navigation.goBack(); } } }; const loadRecentSearches = async () => { try { const savedSearches = await mmkvStorage.getItem(RECENT_SEARCHES_KEY); if (savedSearches) { setRecentSearches(JSON.parse(savedSearches)); } } catch (error) { logger.error('Failed to load recent searches:', error); } }; const saveRecentSearch = async (searchQuery: string) => { try { setRecentSearches(prevSearches => { const newRecentSearches = [ searchQuery, ...prevSearches.filter(s => s !== searchQuery) ].slice(0, MAX_RECENT_SEARCHES); // Save to AsyncStorage mmkvStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches)); return newRecentSearches; }); } catch (error) { logger.error('Failed to save recent search:', error); } }; // Create a stable debounced search function using useMemo const debouncedSearch = useMemo(() => { return debounce(async (searchQuery: string) => { // Cancel any in-flight live search liveSearchHandle.current?.cancel(); liveSearchHandle.current = null; performLiveSearch(searchQuery); }, 800); }, []); // Empty dependency array - create once and never recreate // Track focus state to strictly prevent updates when blurred (fixes Telemetry crash) useFocusEffect( useCallback(() => { isMounted.current = true; return () => { isMounted.current = false; // Cancel any active searches immediately on blur if (liveSearchHandle.current) { liveSearchHandle.current.cancel(); liveSearchHandle.current = null; } debouncedSearch.cancel(); }; }, [debouncedSearch]) ); // Live search implementation const performLiveSearch = async (searchQuery: string) => { // strict guard: don't search if unmounted or blurred if (!isMounted.current) return; if (!searchQuery || searchQuery.trim().length === 0) { setResults({ byAddon: [], allResults: [] }); setSearching(false); return; } setSearching(true); setResults({ byAddon: [], allResults: [] }); // Reset order rank for new search addonOrderRankRef.current = {}; try { if (liveSearchHandle.current) { liveSearchHandle.current.cancel(); } // Pre-fetch addon list to establish a stable order rank const addons = await catalogService.getAllAddons(); // ... (rank logic) ... const rank: Record = {}; let rankCounter = 0; // Cinemeta first rank['com.linvo.cinemeta'] = rankCounter++; // Then others addons.forEach(addon => { if (addon.id !== 'com.linvo.cinemeta') { rank[addon.id] = rankCounter++; } }); addonOrderRankRef.current = rank; const handle = catalogService.startLiveSearch(searchQuery, async (section: AddonSearchResults) => { // Prevent updates if component is unmounted or blurred if (!isMounted.current) return; // Append/update this addon section... setResults(prev => { // ... (existing update logic) ... if (!isMounted.current) return prev; // Extra guard inside setter const getRank = (id: string) => addonOrderRankRef.current[id] ?? Number.MAX_SAFE_INTEGER; // ... (same logic as before) ... const existingIndex = prev.byAddon.findIndex(s => s.addonId === section.addonId); if (existingIndex >= 0) { const copy = prev.byAddon.slice(); copy[existingIndex] = section; return { byAddon: copy, allResults: prev.allResults }; } // Insert new section const insertRank = getRank(section.addonId); let insertAt = prev.byAddon.length; for (let i = 0; i < prev.byAddon.length; i++) { if (getRank(prev.byAddon[i].addonId) > insertRank) { insertAt = i; break; } } const nextByAddon = [ ...prev.byAddon.slice(0, insertAt), section, ...prev.byAddon.slice(insertAt) ]; // Hide loading overlay once first section arrives if (prev.byAddon.length === 0) { setSearching(false); } return { byAddon: nextByAddon, allResults: prev.allResults }; }); try { await saveRecentSearch(searchQuery); } catch { } }); liveSearchHandle.current = handle; await handle.done; if (isMounted.current) { setSearching(false); } } catch (error) { if (isMounted.current) { console.error('Live search error:', error); setSearching(false); } } }; useEffect(() => { // Skip initial mount to prevent unnecessary operations if (isInitialMount.current) { isInitialMount.current = false; loadRecentSearches(); return; } if (query.trim() && query.trim().length >= 2) { setSearching(true); setSearched(true); setShowRecent(false); debouncedSearch(query); } else if (query.trim().length < 2 && query.trim().length > 0) { // Show that we're waiting for more characters setSearching(false); setSearched(false); setShowRecent(false); setResults({ byAddon: [], allResults: [] }); } else { // Cancel any pending search when query is cleared debouncedSearch.cancel(); liveSearchHandle.current?.cancel(); liveSearchHandle.current = null; setResults({ byAddon: [], allResults: [] }); setSearched(false); setSearching(false); setShowRecent(true); loadRecentSearches(); } // Cleanup function to cancel pending searches return () => { debouncedSearch.cancel(); }; }, [query]); // Removed debouncedSearch since it's now stable with useMemo const handleClearSearch = () => { setQuery(''); liveSearchHandle.current?.cancel(); liveSearchHandle.current = null; setResults({ byAddon: [], allResults: [] }); setSearched(false); setShowRecent(true); loadRecentSearches(); inputRef.current?.focus(); }; const renderRecentSearches = () => { if (!showRecent || recentSearches.length === 0) return null; return ( Recent Searches {recentSearches.map((search, index) => ( { setQuery(search); Keyboard.dismiss(); }} > {search} { const newRecentSearches = [...recentSearches]; newRecentSearches.splice(index, 1); setRecentSearches(newRecentSearches); mmkvStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches)); }} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} style={styles.recentSearchDeleteButton} > ))} ); }; // Get available genres for the selected catalog const availableGenres = useMemo(() => { if (!selectedCatalog) return []; return selectedCatalog.genres; }, [selectedCatalog]); // Get catalogs filtered by selected type const filteredCatalogs = useMemo(() => { return discoverCatalogs.filter(catalog => catalog.type === selectedDiscoverType); }, [discoverCatalogs, selectedDiscoverType]); // Handle type selection const handleTypeSelect = (type: 'movie' | 'series') => { setSelectedDiscoverType(type); // Auto-select first catalog for the new type const catalogsForType = discoverCatalogs.filter(c => c.type === type); if (catalogsForType.length > 0) { const firstCatalog = catalogsForType[0]; setSelectedCatalog(firstCatalog); // Auto-select first genre if available if (firstCatalog.genres.length > 0) { setSelectedDiscoverGenre(firstCatalog.genres[0]); } else { setSelectedDiscoverGenre(null); } } else { setSelectedCatalog(null); setSelectedDiscoverGenre(null); } typeSheetRef.current?.dismiss(); }; // Handle catalog selection const handleCatalogSelect = (catalog: DiscoverCatalog) => { setSelectedCatalog(catalog); setSelectedDiscoverGenre(null); // Reset genre when catalog changes catalogSheetRef.current?.dismiss(); }; // Handle genre selection const handleGenreSelect = (genre: string | null) => { setSelectedDiscoverGenre(genre); genreSheetRef.current?.dismiss(); }; // Render backdrop for bottom sheets const renderBackdrop = useCallback( (props: any) => ( ), [] ); // Render discover section with catalog and genre selector chips const renderDiscoverSection = () => { if (query.trim().length > 0) return null; return ( {/* Section Header */} Discover {/* Filter Chips Row */} {/* Filter Chips Row */} {/* Type Selector Chip (Movie/TV Show) */} typeSheetRef.current?.present()} > {selectedDiscoverType === 'movie' ? 'Movies' : 'TV Shows'} {/* Catalog Selector Chip */} catalogSheetRef.current?.present()} > {selectedCatalog ? selectedCatalog.catalogName : 'Select Catalog'} {/* Genre Selector Chip - only show if catalog has genres */} {availableGenres.length > 0 && ( genreSheetRef.current?.present()} > {selectedDiscoverGenre || 'All Genres'} )} {/* Selected filters summary */} {selectedCatalog && ( {selectedCatalog.addonName} • {selectedCatalog.type === 'movie' ? 'Movies' : 'TV Shows'} {selectedDiscoverGenre ? ` • ${selectedDiscoverGenre}` : ''} )} {/* Discover Results */} {discoverLoading ? ( Discovering content... ) : discoverResults.length > 0 ? ( `discover-${item.id}-${index}`} numColumns={isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 3} key={isTV ? 'tv-6' : isLargeTablet ? 'ltab-5' : isTablet ? 'tab-4' : 'phone-3'} columnWrapperStyle={styles.discoverGridRow} contentContainerStyle={styles.discoverGridContent} renderItem={({ item, index }) => ( )} initialNumToRender={9} maxToRenderPerBatch={6} windowSize={5} removeClippedSubviews={true} scrollEnabled={false} ListFooterComponent={ pendingDiscoverResults.length > 0 ? ( Show More ({pendingDiscoverResults.length}) ) : loadingMore ? ( ) : null } /> ) : discoverInitialized && !discoverLoading && selectedCatalog ? ( No content found Try a different genre or catalog ) : !selectedCatalog && discoverInitialized ? ( Select a catalog to discover Tap the catalog chip above to get started ) : null} ); }; const SearchResultItem = ({ item, index, navigation, setSelectedItem, setMenuVisible, currentTheme, isGrid = false }: { item: StreamingContent; index: number; navigation: any; setSelectedItem: (item: StreamingContent) => void; setMenuVisible: (visible: boolean) => void; currentTheme: any; isGrid?: boolean; }) => { const [inLibrary, setInLibrary] = React.useState(!!item.inLibrary); const [watched, setWatched] = React.useState(false); // Calculate dimensions based on poster shape const { itemWidth, aspectRatio } = useMemo(() => { const shape = item.posterShape || 'poster'; const baseHeight = HORIZONTAL_POSTER_HEIGHT; let w = HORIZONTAL_ITEM_WIDTH; let r = 2 / 3; if (isGrid) { // Grid Calculation: (Window Width - Padding) / Columns // Padding: 16 (left) + 16 (right) = 32 // Gap: 12 (between items) * (columns - 1) // Ensure minimum 3 columns on all devices const columns = isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 3; const totalPadding = 32; const totalGap = 12 * (Math.max(3, columns) - 1); const availableWidth = width - totalPadding - totalGap; w = availableWidth / Math.max(3, columns); } else { if (shape === 'landscape') { r = 16 / 9; w = baseHeight * r; } else if (shape === 'square') { r = 1; w = baseHeight; } } return { itemWidth: w, aspectRatio: r }; }, [item.posterShape, isGrid]); React.useEffect(() => { const updateWatched = () => { mmkvStorage.getItem(`watched:${item.type}:${item.id}`).then(val => setWatched(val === 'true')); }; updateWatched(); const sub = DeviceEventEmitter.addListener('watchedStatusChanged', updateWatched); return () => sub.remove(); }, [item.id, item.type]); React.useEffect(() => { const unsubscribe = catalogService.subscribeToLibraryUpdates((items) => { const found = items.find((libItem) => libItem.id === item.id && libItem.type === item.type); setInLibrary(!!found); }); return () => unsubscribe(); }, [item.id, item.type]); return ( { navigation.navigate('Metadata', { id: item.id, type: item.type, addonId: item.addonId }); }} onLongPress={() => { setSelectedItem(item); setMenuVisible(true); // Do NOT toggle refreshFlag here }} delayLongPress={300} activeOpacity={0.7} > {/* Bookmark and watched icons top right, bookmark to the left of watched */} {inLibrary && ( )} {watched && ( )} {/* Rating removed per user request */} {item.name} {item.year && ( {item.year} )} ); }; const hasResultsToShow = useMemo(() => { return results.byAddon.length > 0; }, [results]); // Memoized addon section to prevent re-rendering unchanged sections const AddonSection = React.memo(({ addonGroup, addonIndex }: { addonGroup: AddonSearchResults; addonIndex: number; }) => { const movieResults = useMemo(() => addonGroup.results.filter(item => item.type === 'movie'), [addonGroup.results] ); const seriesResults = useMemo(() => addonGroup.results.filter(item => item.type === 'series'), [addonGroup.results] ); const otherResults = useMemo(() => addonGroup.results.filter(item => item.type !== 'movie' && item.type !== 'series'), [addonGroup.results] ); return ( {/* Addon Header */} {addonGroup.addonName} {addonGroup.results.length} {/* Movies */} {movieResults.length > 0 && ( Movies ({movieResults.length}) ( )} keyExtractor={item => `${addonGroup.addonId}-movie-${item.id}`} horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.horizontalListContent} /> )} {/* TV Shows */} {seriesResults.length > 0 && ( TV Shows ({seriesResults.length}) ( )} keyExtractor={item => `${addonGroup.addonId}-series-${item.id}`} horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.horizontalListContent} /> )} {/* Other types */} {otherResults.length > 0 && ( {otherResults[0].type.charAt(0).toUpperCase() + otherResults[0].type.slice(1)} ({otherResults.length}) ( )} keyExtractor={item => `${addonGroup.addonId}-${item.type}-${item.id}`} horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.horizontalListContent} /> )} ); }, (prev, next) => { // Only re-render if this section's reference changed return prev.addonGroup === next.addonGroup && prev.addonIndex === next.addonIndex; }); // Set up listeners for watched status and library updates // These will trigger re-renders in individual SearchResultItem components useEffect(() => { const watchedSub = DeviceEventEmitter.addListener('watchedStatusChanged', () => { // Individual items will handle their own watched status updates // No need to force a full re-render of all results }); const librarySub = catalogService.subscribeToLibraryUpdates(() => { // Individual items will handle their own library status updates // No need to force a full re-render of all results }); return () => { watchedSub.remove(); librarySub(); }; }, []); return ( {/* ScreenHeader Component */} {/* Search Bar */} {query.length > 0 && ( )} {/* Content Container */} {searching ? ( ) : query.trim().length === 1 ? ( Keep typing... Type at least 2 characters to search ) : searched && !hasResultsToShow ? ( No results found Try different keywords or check your spelling ) : ( { // Only paginate if query is empty (Discover mode) if (query.trim().length > 0 || !settings.showDiscover || pendingDiscoverResults.length > 0) return; const { layoutMeasurement, contentOffset, contentSize } = nativeEvent; const isCloseToBottom = layoutMeasurement.height + contentOffset.y >= contentSize.height - 500; if (isCloseToBottom) { loadMoreDiscoverContent(); } }} > {!query.trim() && renderRecentSearches()} {!query.trim() && settings.showDiscover && renderDiscoverSection()} {/* Render results grouped by addon using memoized component */} {results.byAddon.map((addonGroup, addonIndex) => ( ))} )} {/* DropUpMenu integration for search results */} {selectedItem && ( setMenuVisible(false)} item={selectedItem} isSaved={isSaved} isWatched={isWatched} onOptionSelect={async (option: string) => { if (!selectedItem) return; switch (option) { case 'share': { let url = ''; if (selectedItem.id) { url = `https://www.imdb.com/title/${selectedItem.id}/`; } const message = `${selectedItem.name}\n${url}`; Share.share({ message, url, title: selectedItem.name }); break; } case 'library': { if (isSaved) { await catalogService.removeFromLibrary(selectedItem.type, selectedItem.id); setIsSaved(false); } else { await catalogService.addToLibrary(selectedItem); setIsSaved(true); } break; } case 'watched': { const key = `watched:${selectedItem.type}:${selectedItem.id}`; const newWatched = !isWatched; await mmkvStorage.setItem(key, newWatched ? 'true' : 'false'); setIsWatched(newWatched); break; } default: break; } }} /> )} {/* Catalog Selection Bottom Sheet */} Select Catalog catalogSheetRef.current?.dismiss()}> {filteredCatalogs.map((catalog, index) => ( handleCatalogSelect(catalog)} > {catalog.catalogName} {catalog.addonName} • {catalog.type === 'movie' ? 'Movies' : 'TV Shows'} {catalog.genres.length > 0 ? ` • ${catalog.genres.length} genres` : ''} {selectedCatalog?.catalogId === catalog.catalogId && selectedCatalog?.addonId === catalog.addonId && ( )} ))} {/* Genre Selection Bottom Sheet */} Select Genre genreSheetRef.current?.dismiss()}> {/* All Genres option */} handleGenreSelect(null)} > All Genres Show all content {!selectedDiscoverGenre && ( )} {/* Genre options */} {availableGenres.map((genre, index) => ( handleGenreSelect(genre)} > {genre} {selectedDiscoverGenre === genre && ( )} ))} {/* Type Selection Bottom Sheet */} Select Type typeSheetRef.current?.dismiss()}> {/* Movies option */} handleTypeSelect('movie')} > Movies Browse movie catalogs {selectedDiscoverType === 'movie' && ( )} {/* TV Shows option */} handleTypeSelect('series')} > TV Shows Browse TV series catalogs {selectedDiscoverType === 'series' && ( )} ); }; const styles = StyleSheet.create({ container: { flex: 1, }, contentContainer: { flex: 1, paddingTop: 0, }, searchBarContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8, height: 48, }, searchBarWrapper: { flex: 1, height: 48, }, searchBar: { flexDirection: 'row', alignItems: 'center', borderRadius: 12, paddingHorizontal: 16, height: '100%', shadowColor: "#000", shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.1, shadowRadius: 3.84, elevation: 5, }, searchIcon: { marginRight: 12, }, searchInput: { flex: 1, fontSize: 16, height: '100%', }, clearButton: { padding: 4, }, scrollView: { flex: 1, }, scrollViewContent: { paddingBottom: isTablet ? 120 : 100, // Extra padding for tablet bottom nav paddingHorizontal: 0, }, carouselContainer: { marginBottom: isTablet ? 32 : 24, }, carouselTitle: { fontSize: isTablet ? 20 : 18, fontWeight: '700', marginBottom: isTablet ? 16 : 12, paddingHorizontal: 16, }, carouselSubtitle: { fontSize: isTablet ? 16 : 14, fontWeight: '600', marginBottom: isTablet ? 12 : 8, paddingHorizontal: 16, }, addonHeaderContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: isTablet ? 16 : 12, marginTop: isTablet ? 24 : 16, marginBottom: isTablet ? 8 : 4, borderBottomWidth: 1, borderBottomColor: 'rgba(255,255,255,0.1)', }, addonHeaderIcon: { // removed icon }, addonHeaderText: { fontSize: isTablet ? 18 : 16, fontWeight: '700', flex: 1, }, addonHeaderBadge: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12, }, addonHeaderBadgeText: { fontSize: isTablet ? 12 : 11, fontWeight: '600', }, horizontalListContent: { paddingHorizontal: 16, }, horizontalItem: { width: HORIZONTAL_ITEM_WIDTH, marginRight: 16, }, horizontalItemPosterContainer: { width: HORIZONTAL_ITEM_WIDTH, height: HORIZONTAL_POSTER_HEIGHT, borderRadius: 12, overflow: 'hidden', marginBottom: 8, borderWidth: 1.5, borderColor: 'rgba(255,255,255,0.15)', // Consistent shadow/elevation matching ContentItem elevation: Platform.OS === 'android' ? 1 : 0, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 1, }, horizontalItemPoster: { width: '100%', height: '100%', }, horizontalItemTitle: { fontSize: isTablet ? 12 : 14, fontWeight: '600', lineHeight: isTablet ? 16 : 18, textAlign: 'left', }, yearText: { fontSize: isTablet ? 10 : 12, marginTop: 2, }, recentSearchesContainer: { paddingHorizontal: 16, paddingBottom: isTablet ? 24 : 16, paddingTop: isTablet ? 12 : 8, borderBottomWidth: 1, borderBottomColor: 'rgba(255,255,255,0.05)', marginBottom: isTablet ? 16 : 8, }, recentSearchItem: { flexDirection: 'row', alignItems: 'center', paddingVertical: isTablet ? 12 : 10, paddingHorizontal: 16, marginVertical: 1, }, recentSearchIcon: { marginRight: 12, }, recentSearchText: { fontSize: 16, flex: 1, }, recentSearchDeleteButton: { padding: 4, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, loadingOverlay: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center', zIndex: 5, }, loadingText: { marginTop: 16, fontSize: 16, }, emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: isTablet ? 64 : 32, paddingBottom: isTablet ? 120 : 100, }, emptyText: { fontSize: 18, fontWeight: 'bold', marginTop: 16, marginBottom: 8, }, emptySubtext: { fontSize: 14, textAlign: 'center', lineHeight: 20, }, skeletonContainer: { flexDirection: 'row', flexWrap: 'wrap', paddingHorizontal: 12, paddingTop: 16, justifyContent: 'space-between', }, skeletonVerticalItem: { flexDirection: 'row', marginBottom: 16, }, skeletonPoster: { width: POSTER_WIDTH, height: POSTER_HEIGHT, borderRadius: 12, }, skeletonItemDetails: { flex: 1, marginLeft: 16, justifyContent: 'center', }, skeletonMetaRow: { flexDirection: 'row', gap: 8, marginTop: 8, }, skeletonTitle: { height: 20, width: '80%', marginBottom: 8, borderRadius: 4, }, skeletonMeta: { height: 14, width: '30%', borderRadius: 4, }, skeletonSectionHeader: { height: 24, width: '40%', marginBottom: 16, borderRadius: 4, }, ratingContainer: { position: 'absolute', bottom: 8, right: 8, backgroundColor: 'rgba(0,0,0,0.7)', flexDirection: 'row', alignItems: 'center', paddingHorizontal: 6, paddingVertical: 3, borderRadius: 4, }, ratingText: { fontSize: isTablet ? 9 : 10, fontWeight: '700', marginLeft: 2, }, simpleAnimationContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, simpleAnimationContent: { alignItems: 'center', }, spinnerContainer: { width: 64, height: 64, borderRadius: 32, justifyContent: 'center', alignItems: 'center', marginBottom: 16, shadowColor: "#000", shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.2, shadowRadius: 4, elevation: 3, }, simpleAnimationText: { fontSize: 16, fontWeight: '600', }, watchedIndicator: { position: 'absolute', top: 8, right: 8, borderRadius: 12, padding: 2, zIndex: 2, backgroundColor: 'transparent', }, libraryBadge: { position: 'absolute', top: 8, left: 8, borderRadius: 8, padding: 4, zIndex: 2, backgroundColor: 'transparent', }, // Discover section styles discoverContainer: { paddingTop: isTablet ? 16 : 12, paddingBottom: isTablet ? 24 : 16, }, discoverHeader: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, marginBottom: isTablet ? 16 : 12, gap: 8, }, discoverTitle: { fontSize: isTablet ? 22 : 20, fontWeight: '700', }, discoverTypeContainer: { flexDirection: 'row', paddingHorizontal: 16, marginBottom: isTablet ? 16 : 12, gap: 12, }, discoverTypeButton: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 10, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.1)', gap: 6, }, discoverTypeText: { fontSize: isTablet ? 15 : 14, fontWeight: '600', }, discoverGenreScroll: { marginBottom: isTablet ? 20 : 16, }, discoverGenreContent: { paddingHorizontal: 16, gap: 8, }, discoverGenreChip: { paddingHorizontal: 14, paddingVertical: 8, borderRadius: 16, backgroundColor: 'rgba(255,255,255,0.08)', marginRight: 8, }, discoverGenreChipActive: { backgroundColor: 'rgba(255,255,255,0.2)', }, discoverGenreText: { fontSize: isTablet ? 14 : 13, fontWeight: '500', color: 'rgba(255,255,255,0.7)', }, discoverGenreTextActive: { color: '#FFFFFF', fontWeight: '600', }, discoverLoadingContainer: { alignItems: 'center', justifyContent: 'center', paddingVertical: 60, }, discoverLoadingText: { marginTop: 12, fontSize: 14, }, discoverAddonSection: { marginBottom: isTablet ? 28 : 20, }, discoverAddonHeader: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, marginBottom: isTablet ? 12 : 8, }, discoverAddonName: { fontSize: isTablet ? 16 : 15, fontWeight: '600', flex: 1, }, discoverAddonBadge: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 10, }, discoverAddonBadgeText: { fontSize: 11, fontWeight: '600', }, discoverEmptyContainer: { alignItems: 'center', justifyContent: 'center', paddingVertical: 60, paddingHorizontal: 32, }, discoverEmptyText: { fontSize: 16, fontWeight: '600', marginTop: 12, textAlign: 'center', }, discoverEmptySubtext: { fontSize: 14, marginTop: 4, textAlign: 'center', }, discoverGrid: { flexDirection: 'row', flexWrap: 'wrap', paddingHorizontal: 16, gap: 12, // vertical and horizontal gap }, discoverGridRow: { justifyContent: 'flex-start', gap: 12, }, discoverGridContent: { paddingHorizontal: 16, paddingBottom: 16, }, discoverGridItem: { marginRight: 0, // Override horizontalItem margin marginBottom: 12, }, loadingMoreContainer: { width: '100%', paddingVertical: 16, alignItems: 'center', justifyContent: 'center', }, // New chip-based discover styles discoverChipsScroll: { marginBottom: isTablet ? 12 : 10, flexGrow: 0, }, discoverChipsContent: { paddingHorizontal: 16, flexDirection: 'row', gap: 8, }, discoverSelectorChip: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, gap: 6, }, discoverSelectorText: { fontSize: isTablet ? 14 : 13, fontWeight: '600', }, discoverFilterSummary: { paddingHorizontal: 16, marginBottom: isTablet ? 16 : 12, }, discoverFilterSummaryText: { fontSize: 12, fontWeight: '500', }, // Bottom sheet styles bottomSheetHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 16, borderBottomWidth: 1, borderBottomColor: 'rgba(255,255,255,0.1)', }, bottomSheetTitle: { fontSize: 18, fontWeight: '700', }, bottomSheetContent: { paddingHorizontal: 12, paddingBottom: 40, }, bottomSheetItem: { flexDirection: 'row', alignItems: 'center', paddingVertical: 14, paddingHorizontal: 12, borderRadius: 12, marginVertical: 2, }, bottomSheetItemSelected: { backgroundColor: 'rgba(255,255,255,0.08)', }, bottomSheetItemIcon: { width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.1)', justifyContent: 'center', alignItems: 'center', marginRight: 12, }, bottomSheetItemContent: { flex: 1, }, bottomSheetItemTitle: { fontSize: 16, fontWeight: '600', }, bottomSheetItemSubtitle: { fontSize: 13, marginTop: 2, }, showMoreButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 12, paddingHorizontal: 24, backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: 8, marginVertical: 20, alignSelf: 'center', }, showMoreButtonText: { fontSize: 14, fontWeight: '600', marginRight: 8, }, }); export default SearchScreen;