import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; import { useCallback, useEffect, useRef, useState } from "react"; import { FlatList, View, type ViewProps } from "react-native"; import { Text } from "@/components/common/Text"; import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster"; import { TVPosterCard } from "@/components/tv/TVPosterCard"; import { useScaledTVPosterSizes } from "@/constants/TVPosterSizes"; import { useScaledTVSizes } from "@/constants/TVSizes"; import { useScaledTVTypography } from "@/constants/TVTypography"; const SCALE_PADDING = 20; interface TVSearchSectionProps extends ViewProps { title: string; items: BaseItemDto[]; orientation?: "horizontal" | "vertical"; disabled?: boolean; isFirstSection?: boolean; onItemPress: (item: BaseItemDto) => void; onItemLongPress?: (item: BaseItemDto) => void; imageUrlGetter?: (item: BaseItemDto) => string | undefined; } export const TVSearchSection: React.FC = ({ title, items, orientation = "vertical", disabled = false, isFirstSection = false, onItemPress, onItemLongPress, imageUrlGetter, ...props }) => { const typography = useScaledTVTypography(); const posterSizes = useScaledTVPosterSizes(); const sizes = useScaledTVSizes(); const ITEM_GAP = sizes.gaps.item; const flatListRef = useRef>(null); const [focusedCount, setFocusedCount] = useState(0); const prevFocusedCount = useRef(0); // Track focus count for section useEffect(() => { prevFocusedCount.current = focusedCount; }, [focusedCount]); const handleItemFocus = useCallback(() => { setFocusedCount((c) => c + 1); }, []); const handleItemBlur = useCallback(() => { setFocusedCount((c) => Math.max(0, c - 1)); }, []); const itemWidth = orientation === "horizontal" ? posterSizes.landscape : posterSizes.poster; const getItemLayout = useCallback( (_data: ArrayLike | null | undefined, index: number) => ({ length: itemWidth + ITEM_GAP, offset: (itemWidth + ITEM_GAP) * index, index, }), [itemWidth, ITEM_GAP], ); const renderItem = useCallback( ({ item, index }: { item: BaseItemDto; index: number }) => { const isFirstItem = isFirstSection && index === 0; // Special handling for MusicArtist (circular avatar) if (item.Type === "MusicArtist") { const imageUrl = imageUrlGetter?.(item); return ( onItemPress(item)} onLongPress={ onItemLongPress ? () => onItemLongPress(item) : undefined } hasTVPreferredFocus={isFirstItem && !disabled} onFocus={handleItemFocus} onBlur={handleItemBlur} disabled={disabled} > {imageUrl ? ( ) : ( 👤 )} {item.Name} ); } // Special handling for MusicAlbum, Audio, Playlist (square images) if ( item.Type === "MusicAlbum" || item.Type === "Audio" || item.Type === "Playlist" ) { const imageUrl = imageUrlGetter?.(item); const icon = item.Type === "Playlist" ? "🎶" : item.Type === "Audio" ? "🎵" : "🎵"; return ( onItemPress(item)} onLongPress={ onItemLongPress ? () => onItemLongPress(item) : undefined } hasTVPreferredFocus={isFirstItem && !disabled} onFocus={handleItemFocus} onBlur={handleItemBlur} disabled={disabled} > {imageUrl ? ( ) : ( {icon} )} {item.Name} {item.Type === "MusicAlbum" && ( {item.AlbumArtist || item.Artists?.join(", ")} )} {item.Type === "Audio" && ( {item.Artists?.join(", ") || item.AlbumArtist} )} {item.Type === "Playlist" && ( {item.ChildCount} tracks )} ); } // Use TVPosterCard for all other item types return ( onItemPress(item)} onLongPress={ onItemLongPress ? () => onItemLongPress(item) : undefined } hasTVPreferredFocus={isFirstItem && !disabled} onFocus={handleItemFocus} onBlur={handleItemBlur} disabled={disabled} width={itemWidth} /> ); }, [ orientation, isFirstSection, itemWidth, onItemPress, onItemLongPress, handleItemFocus, handleItemBlur, disabled, imageUrlGetter, posterSizes.poster, typography.callout, ITEM_GAP, ], ); if (!items || items.length === 0) return null; return ( {/* Section Header */} {title} item.Id!} renderItem={renderItem} showsHorizontalScrollIndicator={false} initialNumToRender={5} maxToRenderPerBatch={3} windowSize={5} removeClippedSubviews={false} getItemLayout={getItemLayout} style={{ overflow: "visible" }} contentContainerStyle={{ paddingVertical: SCALE_PADDING, paddingHorizontal: SCALE_PADDING, }} /> ); };