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 ContinueWatchingPoster, {
TV_LANDSCAPE_WIDTH,
} from "@/components/ContinueWatchingPoster.tv";
import { Text } from "@/components/common/Text";
import MoviePoster, {
TV_POSTER_WIDTH,
} from "@/components/posters/MoviePoster.tv";
import SeriesPoster from "@/components/posters/SeriesPoster.tv";
import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster";
import { useScaledTVTypography } from "@/constants/TVTypography";
const ITEM_GAP = 16;
const SCALE_PADDING = 20;
// TV-specific ItemCardText with larger fonts
const TVItemCardText: React.FC<{ item: BaseItemDto }> = ({ item }) => {
const typography = useScaledTVTypography();
return (
{item.Type === "Episode" ? (
<>
{item.Name}
{`S${item.ParentIndexNumber?.toString()}:E${item.IndexNumber?.toString()}`}
{" - "}
{item.SeriesName}
>
) : item.Type === "MusicArtist" ? (
{item.Name}
) : item.Type === "MusicAlbum" ? (
<>
{item.Name}
{item.AlbumArtist || item.Artists?.join(", ")}
>
) : item.Type === "Audio" ? (
<>
{item.Name}
{item.Artists?.join(", ") || item.AlbumArtist}
>
) : item.Type === "Playlist" ? (
<>
{item.Name}
{item.ChildCount} tracks
>
) : item.Type === "Person" ? (
{item.Name}
) : (
<>
{item.Name}
{item.ProductionYear}
>
)}
);
};
interface TVSearchSectionProps extends ViewProps {
title: string;
items: BaseItemDto[];
orientation?: "horizontal" | "vertical";
disabled?: boolean;
isFirstSection?: boolean;
onItemPress: (item: BaseItemDto) => void;
imageUrlGetter?: (item: BaseItemDto) => string | undefined;
}
export const TVSearchSection: React.FC = ({
title,
items,
orientation = "vertical",
disabled = false,
isFirstSection = false,
onItemPress,
imageUrlGetter,
...props
}) => {
const typography = useScaledTVTypography();
const flatListRef = useRef>(null);
const [focusedCount, setFocusedCount] = useState(0);
const prevFocusedCount = useRef(0);
// When section loses all focus, scroll back to start
useEffect(() => {
if (prevFocusedCount.current > 0 && focusedCount === 0) {
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
}
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" ? TV_LANDSCAPE_WIDTH : TV_POSTER_WIDTH;
const getItemLayout = useCallback(
(_data: ArrayLike | null | undefined, index: number) => ({
length: itemWidth + ITEM_GAP,
offset: (itemWidth + ITEM_GAP) * index,
index,
}),
[itemWidth],
);
const renderItem = useCallback(
({ item, index }: { item: BaseItemDto; index: number }) => {
const isFirstItem = isFirstSection && index === 0;
const isHorizontal = orientation === "horizontal";
const renderPoster = () => {
// Music Artist - circular avatar
if (item.Type === "MusicArtist") {
const imageUrl = imageUrlGetter?.(item);
return (
{imageUrl ? (
) : (
👤
)}
);
}
// Music Album, 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 (
{imageUrl ? (
) : (
{icon}
)}
);
}
// Person (Actor)
if (item.Type === "Person") {
return ;
}
// Episode rendering
if (item.Type === "Episode" && isHorizontal) {
return ;
}
if (item.Type === "Episode" && !isHorizontal) {
return ;
}
// Movie rendering
if (item.Type === "Movie" && isHorizontal) {
return ;
}
if (item.Type === "Movie" && !isHorizontal) {
return ;
}
// Series rendering
if (item.Type === "Series" && !isHorizontal) {
return ;
}
if (item.Type === "Series" && isHorizontal) {
return ;
}
// BoxSet (Collection)
if (item.Type === "BoxSet" && !isHorizontal) {
return ;
}
if (item.Type === "BoxSet" && isHorizontal) {
return ;
}
// Default fallback
return isHorizontal ? (
) : (
);
};
// Special width for music artists (circular)
const actualItemWidth = item.Type === "MusicArtist" ? 160 : itemWidth;
return (
onItemPress(item)}
hasTVPreferredFocus={isFirstItem && !disabled}
onFocus={handleItemFocus}
onBlur={handleItemBlur}
disabled={disabled}
>
{renderPoster()}
);
},
[
orientation,
isFirstSection,
itemWidth,
onItemPress,
handleItemFocus,
handleItemBlur,
disabled,
imageUrlGetter,
],
);
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,
}}
/>
);
};