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, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; import { catalogService, StreamingContent } from '../services/catalogService'; import { Image } from 'expo-image'; import debounce from 'lodash/debounce'; import { DropUpMenu } from '../components/home/DropUpMenu'; import { DeviceEventEmitter, Share } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; 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'; const { width, height } = Dimensions.get('window'); const isTablet = width >= 768; const TAB_BAR_HEIGHT = 85; // Tablet-optimized poster sizes const HORIZONTAL_ITEM_WIDTH = isTablet ? width * 0.18 : width * 0.3; const HORIZONTAL_POSTER_HEIGHT = HORIZONTAL_ITEM_WIDTH * 1.5; const POSTER_WIDTH = isTablet ? 70 : 90; const POSTER_HEIGHT = isTablet ? 105 : 135; const RECENT_SEARCHES_KEY = 'recent_searches'; const MAX_RECENT_SEARCHES = 10; const PLACEHOLDER_POSTER = 'https://placehold.co/300x450/222222/CCCCCC?text=No+Poster'; const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity); const SkeletonLoader = () => { const pulseAnim = React.useRef(new RNAnimated.Value(0)).current; const { currentTheme } = useTheme(); React.useEffect(() => { const pulse = RNAnimated.loop( RNAnimated.sequence([ RNAnimated.timing(pulseAnim, { toValue: 1, duration: 1000, useNativeDriver: true, }), RNAnimated.timing(pulseAnim, { toValue: 0, duration: 1000, useNativeDriver: true, }), ]) ); pulse.start(); return () => pulse.stop(); }, [pulseAnim]); const opacity = pulseAnim.interpolate({ inputRange: [0, 1], outputRange: [0.3, 0.7], }); const renderSkeletonItem = () => ( ); return ( {[...Array(5)].map((_, index) => ( {index === 0 && ( )} {renderSkeletonItem()} ))} ); }; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; // Create a simple, elegant animation component const SimpleSearchAnimation = () => { // Simple animation values that work reliably const spinAnim = React.useRef(new RNAnimated.Value(0)).current; const fadeAnim = React.useRef(new RNAnimated.Value(0)).current; const { currentTheme } = useTheme(); React.useEffect(() => { // Rotation animation const spin = RNAnimated.loop( RNAnimated.timing(spinAnim, { toValue: 1, duration: 1500, easing: Easing.linear, useNativeDriver: true, }) ); // Fade animation const fade = RNAnimated.timing(fadeAnim, { toValue: 1, duration: 300, useNativeDriver: true, }); // Start animations spin.start(); fade.start(); // Clean up return () => { spin.stop(); }; }, [spinAnim, fadeAnim]); // Simple rotation interpolation const spin = spinAnim.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '360deg'], }); return ( Searching ); }; const SearchScreen = () => { const navigation = useNavigation>(); const isDarkMode = true; const [query, setQuery] = useState(''); const [results, setResults] = useState([]); 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(); // 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 AsyncStorage.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([]); 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 AsyncStorage.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 AsyncStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches)); return newRecentSearches; }); } catch (error) { logger.error('Failed to save recent search:', error); } }; const debouncedSearch = useCallback( debounce(async (searchQuery: string) => { if (!searchQuery.trim()) { setResults([]); setSearching(false); return; } try { logger.info('Performing search for:', searchQuery); const searchResults = await catalogService.searchContentCinemeta(searchQuery); setResults(searchResults); if (searchResults.length > 0) { await saveRecentSearch(searchQuery); } logger.info('Search completed, found', searchResults.length, 'results'); } catch (error) { logger.error('Search failed:', error); setResults([]); } finally { setSearching(false); } }, 800), [] ); useEffect(() => { 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([]); } else { // Cancel any pending search when query is cleared debouncedSearch.cancel(); setResults([]); setSearched(false); setSearching(false); setShowRecent(true); loadRecentSearches(); } // Cleanup function to cancel pending searches return () => { debouncedSearch.cancel(); }; }, [query, debouncedSearch]); const handleClearSearch = () => { setQuery(''); setResults([]); 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(); }} entering={FadeIn.duration(300).delay(index * 50)} > {search} { const newRecentSearches = [...recentSearches]; newRecentSearches.splice(index, 1); setRecentSearches(newRecentSearches); AsyncStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches)); }} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} style={styles.recentSearchDeleteButton} > ))} ); }; const SearchResultItem = ({ item, index, navigation, setSelectedItem, setMenuVisible, currentTheme, refreshFlag }: { item: StreamingContent; index: number; navigation: any; setSelectedItem: (item: StreamingContent) => void; setMenuVisible: (visible: boolean) => void; currentTheme: any; refreshFlag: boolean; }) => { const [inLibrary, setInLibrary] = React.useState(!!item.inLibrary); const [watched, setWatched] = React.useState(false); // Re-check status when refreshFlag changes React.useEffect(() => { AsyncStorage.getItem(`watched:${item.type}:${item.id}`).then(val => setWatched(val === 'true')); const items = catalogService.getLibraryItems(); const found = items.find((libItem: any) => libItem.id === item.id && libItem.type === item.type); setInLibrary(!!found); }, [refreshFlag, item.id, item.type]); React.useEffect(() => { const updateWatched = () => { AsyncStorage.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 }); }} onLongPress={() => { setSelectedItem(item); setMenuVisible(true); // Do NOT toggle refreshFlag here }} delayLongPress={300} entering={FadeIn.duration(300).delay(index * 50)} activeOpacity={0.7} > {/* Bookmark and watched icons top right, bookmark to the left of watched */} {inLibrary && ( )} {watched && ( )} {/* 'series'/'movie' text in original place */} {item.type === 'movie' ? 'MOVIE' : 'SERIES'} {item.imdbRating && ( {item.imdbRating} )} {item.name} {item.year && ( {item.year} )} ); }; const movieResults = useMemo(() => { return results.filter(item => item.type === 'movie'); }, [results]); const seriesResults = useMemo(() => { return results.filter(item => item.type === 'series'); }, [results]); const hasResultsToShow = useMemo(() => { return movieResults.length > 0 || seriesResults.length > 0; }, [movieResults, seriesResults]); const headerBaseHeight = Platform.OS === 'android' ? 80 : 60; const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top; const headerHeight = headerBaseHeight + topSpacing + 60; useEffect(() => { const watchedSub = DeviceEventEmitter.addListener('watchedStatusChanged', () => setRefreshFlag(f => !f)); const librarySub = catalogService.subscribeToLibraryUpdates(() => setRefreshFlag(f => !f)); const focusSub = navigation.addListener('focus', () => setRefreshFlag(f => !f)); return () => { watchedSub.remove(); librarySub(); focusSub(); }; }, []); return ( {/* Fixed position header background to prevent shifts */} {/* Header Section with proper top spacing */} Search {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 ) : ( {!query.trim() && renderRecentSearches()} {movieResults.length > 0 && ( Movies ({movieResults.length}) ( )} keyExtractor={item => `movie-${item.id}`} horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.horizontalListContent} extraData={refreshFlag} /> )} {seriesResults.length > 0 && ( TV Shows ({seriesResults.length}) ( )} keyExtractor={item => `series-${item.id}`} horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.horizontalListContent} extraData={refreshFlag} /> )} )} {/* 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 AsyncStorage.setItem(key, newWatched ? 'true' : 'false'); setIsWatched(newWatched); break; } default: break; } }} /> )} ); }; const styles = StyleSheet.create({ container: { flex: 1, }, headerBackground: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 1, }, contentContainer: { flex: 1, paddingTop: 0, }, header: { paddingHorizontal: 15, justifyContent: 'flex-end', paddingBottom: 0, backgroundColor: 'transparent', zIndex: 2, }, headerTitle: { fontSize: 32, fontWeight: '800', letterSpacing: 0.5, marginBottom: 12, }, 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, }, horizontalListContent: { paddingHorizontal: isTablet ? 16 : 12, paddingRight: isTablet ? 12 : 8, }, horizontalItem: { width: HORIZONTAL_ITEM_WIDTH, marginRight: isTablet ? 16 : 12, }, horizontalItemPosterContainer: { width: HORIZONTAL_ITEM_WIDTH, height: HORIZONTAL_POSTER_HEIGHT, borderRadius: 16, overflow: 'hidden', marginBottom: 8, borderWidth: 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, }, itemTypeContainer: { position: 'absolute', top: 8, left: 8, backgroundColor: 'rgba(0,0,0,0.7)', paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, }, itemTypeText: { fontSize: isTablet ? 7 : 8, fontWeight: '700', }, 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, right: 36, borderRadius: 8, padding: 4, zIndex: 2, backgroundColor: 'transparent', }, }); export default SearchScreen;