diff --git a/src/components/common/ScreenHeader.tsx b/src/components/common/ScreenHeader.tsx new file mode 100644 index 00000000..6b675d0f --- /dev/null +++ b/src/components/common/ScreenHeader.tsx @@ -0,0 +1,240 @@ +import React from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + StatusBar, + Platform, +} from 'react-native'; +import { useTheme } from '../../contexts/ThemeContext'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Feather, MaterialIcons } from '@expo/vector-icons'; + +const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; + +interface ScreenHeaderProps { + /** + * The main title displayed in the header + */ + title: string; + /** + * Optional right action button (icon name from Feather icons) + */ + rightActionIcon?: string; + /** + * Optional callback for right action button press + */ + onRightActionPress?: () => void; + /** + * Optional custom right action component (overrides rightActionIcon) + */ + rightActionComponent?: React.ReactNode; + /** + * Optional back button (shows arrow back icon) + */ + showBackButton?: boolean; + /** + * Optional callback for back button press + */ + onBackPress?: () => void; + /** + * Whether this screen is displayed on a tablet layout + */ + isTablet?: boolean; + /** + * Optional extra top padding for tablet navigation offset + */ + tabletNavOffset?: number; + /** + * Optional custom title component (overrides title text) + */ + titleComponent?: React.ReactNode; + /** + * Optional children to render below the title row (e.g., filters, search bar) + */ + children?: React.ReactNode; + /** + * Whether to hide the header title row (useful when showing only children) + */ + hideTitleRow?: boolean; + /** + * Use MaterialIcons instead of Feather for icons + */ + useMaterialIcons?: boolean; + /** + * Optional custom style for title + */ + titleStyle?: object; +} + +const ScreenHeader: React.FC = ({ + title, + rightActionIcon, + onRightActionPress, + rightActionComponent, + showBackButton = false, + onBackPress, + isTablet = false, + tabletNavOffset = 64, + titleComponent, + children, + hideTitleRow = false, + useMaterialIcons = false, + titleStyle, +}) => { + const { currentTheme } = useTheme(); + const insets = useSafeAreaInsets(); + + // Calculate header spacing + const topSpacing = + (Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT : insets.top) + + (isTablet ? tabletNavOffset : 0); + + const headerBaseHeight = Platform.OS === 'android' ? 80 : 60; + const titleRowHeight = headerBaseHeight + topSpacing; + + const IconComponent = useMaterialIcons ? MaterialIcons : Feather; + const backIconName = useMaterialIcons ? 'arrow-back' : 'arrow-left'; + + return ( + <> + {/* Fixed position header background to prevent shifts */} + + + {/* Header Section */} + + {/* Title Row */} + {!hideTitleRow && ( + + + {showBackButton ? ( + + + + ) : null} + + {titleComponent ? ( + titleComponent + ) : ( + + {title} + + )} + + {/* Right Action */} + {rightActionComponent ? ( + {rightActionComponent} + ) : rightActionIcon && onRightActionPress ? ( + + + + ) : ( + + )} + + + )} + + {/* Children (filters, search bar, etc.) */} + {children} + + + ); +}; + +const styles = StyleSheet.create({ + headerBackground: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + zIndex: 10, + }, + header: { + paddingHorizontal: 20, + zIndex: 11, + }, + titleRow: { + justifyContent: 'flex-end', + paddingBottom: 8, + }, + headerContent: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + backButton: { + padding: 8, + marginLeft: -8, + marginRight: 8, + }, + headerTitle: { + fontSize: 32, + fontWeight: '800', + letterSpacing: 0.5, + flex: 1, + }, + headerTitleWithBack: { + fontSize: 24, + flex: 0, + }, + rightActionContainer: { + minWidth: 40, + alignItems: 'flex-end', + }, + rightActionButton: { + padding: 8, + marginRight: -8, + }, + rightActionPlaceholder: { + width: 40, + }, +}); + +export default ScreenHeader; diff --git a/src/screens/DownloadsScreen.tsx b/src/screens/DownloadsScreen.tsx index db0fedc3..96710d3e 100644 --- a/src/screens/DownloadsScreen.tsx +++ b/src/screens/DownloadsScreen.tsx @@ -34,6 +34,7 @@ import { VideoPlayerService } from '../services/videoPlayerService'; import type { DownloadItem } from '../contexts/DownloadsContext'; import { useToast } from '../contexts/ToastContext'; import CustomAlert from '../components/CustomAlert'; +import ScreenHeader from '../components/common/ScreenHeader'; const { height, width } = Dimensions.get('window'); const isTablet = width >= 768; @@ -346,7 +347,6 @@ const DownloadsScreen: React.FC = () => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); const { settings } = useSettings(); - const { top: safeAreaTop } = useSafeAreaInsets(); const { downloads, pauseDownload, resumeDownload, cancelDownload } = useDownloads(); const { showSuccess, showInfo } = useToast(); @@ -356,9 +356,6 @@ const DownloadsScreen: React.FC = () => { const [showRemoveAlert, setShowRemoveAlert] = useState(false); const [pendingRemoveItem, setPendingRemoveItem] = useState(null); - // Animation values - const headerOpacity = useSharedValue(1); - // Filter downloads based on selected filter const filteredDownloads = useMemo(() => { if (selectedFilter === 'all') return downloads; @@ -571,11 +568,6 @@ const DownloadsScreen: React.FC = () => { }, []) ); - // Animated styles - const headerStyle = useAnimatedStyle(() => ({ - opacity: headerOpacity.value, - })); - const renderFilterButton = (filter: typeof selectedFilter, label: string, count: number) => ( { backgroundColor="transparent" /> - {/* Header */} - - - - Downloads - + {/* ScreenHeader Component */} + { color={currentTheme.colors.mediumEmphasis} /> - - + } + isTablet={isTablet} + > {downloads.length > 0 && ( {renderFilterButton('all', 'All', stats.total)} @@ -669,7 +650,7 @@ const DownloadsScreen: React.FC = () => { {renderFilterButton('paused', 'Paused', stats.paused)} )} - + {/* Content */} {downloads.length === 0 ? ( @@ -742,23 +723,6 @@ const styles = StyleSheet.create({ container: { flex: 1, }, - header: { - paddingHorizontal: isTablet ? 24 : Math.max(1, width * 0.05), - paddingBottom: isTablet ? 20 : 16, - borderBottomWidth: StyleSheet.hairlineWidth, - }, - headerTitleRow: { - flexDirection: 'row', - alignItems: 'flex-end', - justifyContent: 'space-between', - marginBottom: isTablet ? 20 : 16, - paddingBottom: 8, - }, - headerTitle: { - fontSize: isTablet ? 36 : Math.min(32, width * 0.08), - fontWeight: '800', - letterSpacing: 0.3, - }, helpButton: { padding: 8, marginLeft: 8, diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index 470575e9..36d5103d 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -4,6 +4,7 @@ import { Share } from 'react-native'; import { mmkvStorage } from '../services/mmkvStorage'; import { useToast } from '../contexts/ToastContext'; import DropUpMenu from '../components/home/DropUpMenu'; +import ScreenHeader from '../components/common/ScreenHeader'; import { View, Text, @@ -107,7 +108,7 @@ const TraktItem = React.memo(({ item, width, navigation, currentTheme }: { item: navigation.navigate('Metadata', { id: item.imdbId, type: item.type }); } }, [navigation, item.imdbId, item.type]); - + return ( { const renderSkeletonItem = () => ( - - ); @@ -212,7 +213,7 @@ const LibraryScreen = () => { const [selectedItem, setSelectedItem] = useState(null); const insets = useSafeAreaInsets(); const { currentTheme } = useTheme(); - + // Trakt integration const { isAuthenticated: traktAuthenticated, @@ -272,14 +273,14 @@ const LibraryScreen = () => { setLoading(true); try { const items = await catalogService.getLibraryItems(); - + // Sort by date added (most recent first) const sortedItems = items.sort((a, b) => { const timeA = (a as any).addedToLibraryAt || 0; const timeB = (b as any).addedToLibraryAt || 0; return timeB - timeA; // Descending order (newest first) }); - + // Load watched status for each item from AsyncStorage const updatedItems = await Promise.all(sortedItems.map(async (item) => { // Map StreamingContent to LibraryItem shape @@ -313,7 +314,7 @@ const LibraryScreen = () => { const timeB = (b as any).addedToLibraryAt || 0; return timeB - timeA; // Descending order (newest first) }); - + // Sync watched status on update const updatedItems = await Promise.all(sortedItems.map(async (item) => { // Map StreamingContent to LibraryItem shape @@ -403,8 +404,8 @@ const LibraryScreen = () => { activeOpacity={0.7} > - - + { )} - + {item.name} @@ -444,11 +445,11 @@ const LibraryScreen = () => { > - {folder.name} @@ -724,8 +725,8 @@ const LibraryScreen = () => { Your Trakt collections will appear here once you start using Trakt - { // Show collection folders return ( - renderTraktCollectionFolder({ folder: item })} keyExtractor={item => item.id} numColumns={numColumns} contentContainerStyle={styles.listContainer} showsVerticalScrollIndicator={false} - onEndReachedThreshold={0.7} - onEndReached={() => {}} + onEndReachedThreshold={0.7} + onEndReached={() => { }} /> ); } // Show content for specific folder const folderItems = getTraktFolderItems(selectedTraktFolder); - + if (folderItems.length === 0) { const folderName = traktFolders.find(f => f.id === selectedTraktFolder)?.name || 'Collection'; return ( @@ -767,8 +768,8 @@ const LibraryScreen = () => { This collection is empty - { contentContainerStyle={{ paddingBottom: insets.bottom + 80 }} showsVerticalScrollIndicator={false} onEndReachedThreshold={0.7} - onEndReached={() => {}} + onEndReached={() => { }} /> ); }; const renderFilter = (filterType: 'trakt' | 'movies' | 'series', label: string, iconName: keyof typeof MaterialIcons.glyphMap) => { const isActive = filter === filterType; - + return ( { const emptySubtitle = 'Add some content to your library to see it here'; return ( - @@ -869,8 +870,8 @@ const LibraryScreen = () => { {emptySubtitle} - { contentContainerStyle={styles.listContainer} showsVerticalScrollIndicator={false} onEndReachedThreshold={0.7} - onEndReached={() => {}} + onEndReached={() => { }} /> ); }; - const headerBaseHeight = Platform.OS === 'android' ? 80 : 60; // Tablet detection aligned with navigation tablet logic const isTablet = useMemo(() => { const smallestDimension = Math.min(width, height); return (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768); }, [width, height]); - // Keep header below floating top navigator on tablets - const tabletNavOffset = isTablet ? 64 : 0; - const topSpacing = (Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top) + tabletNavOffset; - const headerHeight = headerBaseHeight + topSpacing; return ( - {/* Fixed position header background to prevent shifts */} - - - - {/* Header Section with proper top spacing */} - - - {showTraktContent ? ( - <> - { - if (selectedTraktFolder) { - setSelectedTraktFolder(null); - } else { - setShowTraktContent(false); - } - }} - activeOpacity={0.7} - > - - - - {selectedTraktFolder - ? traktFolders.find(f => f.id === selectedTraktFolder)?.name || 'Collection' - : 'Trakt Collection' - } - - - ) : ( - <> - Library - navigation.navigate('Calendar')} - activeOpacity={0.7} - > - - - - )} - - + {/* ScreenHeader Component */} + f.id === selectedTraktFolder)?.name || 'Collection' + : 'Trakt Collection') + : 'Library' + } + showBackButton={showTraktContent} + onBackPress={showTraktContent ? () => { + if (selectedTraktFolder) { + setSelectedTraktFolder(null); + } else { + setShowTraktContent(false); + } + } : undefined} + useMaterialIcons={showTraktContent} + rightActionIcon={!showTraktContent ? 'calendar' : undefined} + onRightActionPress={!showTraktContent ? () => navigation.navigate('Calendar') : undefined} + isTablet={isTablet} + /> - {/* Content Container */} - - {!showTraktContent && ( - // Replaced ScrollView with View and used the modified style - - {renderFilter('trakt', 'Trakt', 'pan-tool')} - {renderFilter('movies', 'Movies', 'movie')} - {renderFilter('series', 'TV Shows', 'live-tv')} - - )} - - {showTraktContent ? renderTraktContent() : renderContent()} + {/* Content Container */} + + {!showTraktContent && ( + + {renderFilter('trakt', 'Trakt', 'pan-tool')} + {renderFilter('movies', 'Movies', 'movie')} + {renderFilter('series', 'TV Shows', 'live-tv')} - + )} + + {showTraktContent ? renderTraktContent() : renderContent()} + {/* DropUpMenu integration */} {selectedItem && ( @@ -991,45 +953,45 @@ const LibraryScreen = () => { if (!selectedItem) return; switch (option) { case 'library': { - try { - await catalogService.removeFromLibrary(selectedItem.type, selectedItem.id); - showInfo('Removed from Library', 'Item removed from your library'); - setLibraryItems(prev => prev.filter(item => !(item.id === selectedItem.id && item.type === selectedItem.type))); - setMenuVisible(false); - } catch (error) { - showError('Failed to update Library', 'Unable to remove item from library'); - } - break; + try { + await catalogService.removeFromLibrary(selectedItem.type, selectedItem.id); + showInfo('Removed from Library', 'Item removed from your library'); + setLibraryItems(prev => prev.filter(item => !(item.id === selectedItem.id && item.type === selectedItem.type))); + setMenuVisible(false); + } catch (error) { + showError('Failed to update Library', 'Unable to remove item from library'); + } + break; } case 'watched': { - try { - // Use AsyncStorage to store watched status by key - const key = `watched:${selectedItem.type}:${selectedItem.id}`; - const newWatched = !selectedItem.watched; - await mmkvStorage.setItem(key, newWatched ? 'true' : 'false'); - showInfo(newWatched ? 'Marked as Watched' : 'Marked as Unwatched', newWatched ? 'Item marked as watched' : 'Item marked as unwatched'); - // Instantly update local state - setLibraryItems(prev => prev.map(item => - item.id === selectedItem.id && item.type === selectedItem.type - ? { ...item, watched: newWatched } - : item - )); - } catch (error) { - showError('Failed to update watched status', 'Unable to update watched status'); - } - break; + try { + // Use AsyncStorage to store watched status by key + const key = `watched:${selectedItem.type}:${selectedItem.id}`; + const newWatched = !selectedItem.watched; + await mmkvStorage.setItem(key, newWatched ? 'true' : 'false'); + showInfo(newWatched ? 'Marked as Watched' : 'Marked as Unwatched', newWatched ? 'Item marked as watched' : 'Item marked as unwatched'); + // Instantly update local state + setLibraryItems(prev => prev.map(item => + item.id === selectedItem.id && item.type === selectedItem.type + ? { ...item, watched: newWatched } + : item + )); + } catch (error) { + showError('Failed to update watched status', 'Unable to update watched status'); + } + break; } 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; + 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; } default: - break; + break; } }} /> @@ -1042,13 +1004,6 @@ const styles = StyleSheet.create({ container: { flex: 1, }, - headerBackground: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - zIndex: 1, - }, watchedIndicator: { position: 'absolute', top: 8, @@ -1060,23 +1015,6 @@ const styles = StyleSheet.create({ contentContainer: { flex: 1, }, - header: { - paddingHorizontal: 20, - justifyContent: 'flex-end', - paddingBottom: 8, - backgroundColor: 'transparent', - zIndex: 2, - }, - headerContent: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - headerTitle: { - fontSize: 32, - fontWeight: '800', - letterSpacing: 0.5, - }, filtersContainer: { flexDirection: 'row', justifyContent: 'center', @@ -1130,7 +1068,7 @@ const styles = StyleSheet.create({ borderRadius: 12, overflow: 'hidden', backgroundColor: 'rgba(255,255,255,0.03)', - aspectRatio: 2/3, + aspectRatio: 2 / 3, elevation: 5, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, @@ -1253,7 +1191,7 @@ const styles = StyleSheet.create({ borderRadius: 8, overflow: 'hidden', backgroundColor: 'rgba(255,255,255,0.03)', - aspectRatio: 2/3, + aspectRatio: 2 / 3, elevation: 5, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, diff --git a/src/screens/SearchScreen.tsx b/src/screens/SearchScreen.tsx index 04b74d88..13d9a5cc 100644 --- a/src/screens/SearchScreen.tsx +++ b/src/screens/SearchScreen.tsx @@ -27,11 +27,11 @@ 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, +import Animated, { + FadeIn, + FadeOut, + useAnimatedStyle, + useSharedValue, withTiming, interpolate, withSpring, @@ -43,6 +43,7 @@ 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'; const { width, height } = Dimensions.get('window'); @@ -110,21 +111,21 @@ const SkeletonLoader = () => { const renderSkeletonItem = () => ( @@ -138,7 +139,7 @@ const SkeletonLoader = () => { {index === 0 && ( )} @@ -157,7 +158,7 @@ const SimpleSearchAnimation = () => { 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( @@ -168,32 +169,32 @@ const SimpleSearchAnimation = () => { 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 ( - { styles.spinnerContainer, { transform: [{ rotate: spin }], backgroundColor: currentTheme.colors.primary } ]}> - Searching @@ -268,9 +269,9 @@ const SearchScreen = () => { StatusBar.setBackgroundColor('transparent'); } }; - + applyStatusBarConfig(); - + // Re-apply on focus const unsubscribe = navigation.addListener('focus', applyStatusBarConfig); return unsubscribe; @@ -284,7 +285,7 @@ const SearchScreen = () => { useEffect(() => { loadRecentSearches(); - + // Cleanup function to cancel pending searches on unmount return () => { debouncedSearch.cancel(); @@ -302,12 +303,12 @@ const SearchScreen = () => { return { opacity: backButtonOpacity.value, transform: [ - { + { translateX: interpolate( backButtonOpacity.value, [0, 1], [-20, 0] - ) + ) } ] }; @@ -361,14 +362,14 @@ const SearchScreen = () => { const saveRecentSearch = async (searchQuery: string) => { try { setRecentSearches(prevSearches => { - const newRecentSearches = [ - searchQuery, + const newRecentSearches = [ + searchQuery, ...prevSearches.filter(s => s !== searchQuery) - ].slice(0, MAX_RECENT_SEARCHES); - + ].slice(0, MAX_RECENT_SEARCHES); + // Save to AsyncStorage mmkvStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches)); - + return newRecentSearches; }); } catch (error) { @@ -400,7 +401,7 @@ const SearchScreen = () => { const rank: Record = {}; addons.forEach((a, idx) => { rank[a.id] = idx; }); addonOrderRankRef.current = rank; - } catch {} + } catch { } const handle = catalogService.startLiveSearch(searchQuery, async (section: AddonSearchResults) => { // Append/update this addon section immediately with minimal changes @@ -444,7 +445,7 @@ const SearchScreen = () => { // Save to recents after first result batch try { await saveRecentSearch(searchQuery); - } catch {} + } catch { } }); liveSearchHandle.current = handle; }, 800); @@ -502,7 +503,7 @@ const SearchScreen = () => { if (!showRecent || recentSearches.length === 0) return null; return ( - @@ -586,10 +587,10 @@ const SearchScreen = () => { entering={FadeIn.duration(300).delay(index * 50)} activeOpacity={0.7} > - + }]}> { /> {/* Bookmark and watched icons top right, bookmark to the left of watched */} {inLibrary && ( - + )} {watched && ( - + )} {item.imdbRating && ( - + {item.imdbRating} )} - { {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; + const AddonSection = React.memo(({ + addonGroup, + addonIndex + }: { + addonGroup: AddonSearchResults; addonIndex: number; }) => { - const movieResults = useMemo(() => - addonGroup.results.filter(item => item.type === 'movie'), + const movieResults = useMemo(() => + addonGroup.results.filter(item => item.type === 'movie'), [addonGroup.results] ); - const seriesResults = useMemo(() => - addonGroup.results.filter(item => item.type === 'series'), + 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'), + const otherResults = useMemo(() => + addonGroup.results.filter(item => item.type !== 'movie' && item.type !== 'series'), [addonGroup.results] ); @@ -679,15 +680,15 @@ const SearchScreen = () => { {/* Movies */} {movieResults.length > 0 && ( - + Movies ({movieResults.length}) { {/* TV Shows */} {seriesResults.length > 0 && ( - + TV Shows ({seriesResults.length}) { {/* Other types */} {otherResults.length > 0 && ( - + {otherResults[0].type.charAt(0).toUpperCase() + otherResults[0].type.slice(1)} ({otherResults.length}) { return prev.addonGroup === next.addonGroup && prev.addonIndex === next.addonIndex; }); - const headerBaseHeight = Platform.OS === 'android' ? 80 : 60; - // Keep header below floating top navigator on tablets by adding extra offset - const tabletNavOffset = (isTV || isLargeTablet || isTablet) ? 64 : 0; - const topSpacing = (Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top) + tabletNavOffset; - const headerHeight = headerBaseHeight + topSpacing + 60; - // Set up listeners for watched status and library updates // These will trigger re-renders in individual SearchResultItem components useEffect(() => { @@ -809,11 +804,11 @@ const SearchScreen = () => { }, []); return ( - @@ -822,172 +817,170 @@ const SearchScreen = () => { backgroundColor="transparent" translucent /> - {/* Fixed position header background to prevent shifts */} - - - {/* Header Section with proper top spacing */} - - Search - + + {/* ScreenHeader Component */} + + {/* Search Bar */} + + - - - - {query.length > 0 && ( - - - - )} - + + + {query.length > 0 && ( + + + + )} - {/* Content Container */} - - {searching ? ( - - + + {/* 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()} + {/* Render results grouped by addon using memoized component */} + {results.byAddon.map((addonGroup, addonIndex) => ( + - - ) : 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()} - {/* 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; - } - }} - /> + ))} + )} + {/* 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; + } + }} + /> + )} ); }; @@ -996,30 +989,10 @@ 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',