diff --git a/components/home/Home.tv.tsx b/components/home/Home.tv.tsx index 6ba3fb23..28b0a79f 100644 --- a/components/home/Home.tv.tsx +++ b/components/home/Home.tv.tsx @@ -739,6 +739,7 @@ export const Home = () => { } isFirstSection={isFirstSection} onItemFocus={handleItemFocus} + parentId={section.parentId} /> {streamystatsSections} diff --git a/components/home/InfiniteScrollingCollectionList.tv.tsx b/components/home/InfiniteScrollingCollectionList.tv.tsx index ce7e5cbf..8ce25691 100644 --- a/components/home/InfiniteScrollingCollectionList.tv.tsx +++ b/components/home/InfiniteScrollingCollectionList.tv.tsx @@ -1,3 +1,4 @@ +import { Ionicons } from "@expo/vector-icons"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { type QueryFunction, @@ -21,6 +22,7 @@ import MoviePoster, { import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster"; import { Colors } from "@/constants/Colors"; import useRouter from "@/hooks/useAppRouter"; +import { SortByOption, SortOrderOption } from "@/utils/atoms/filters"; import ContinueWatchingPoster, { TV_LANDSCAPE_WIDTH, } from "../ContinueWatchingPoster.tv"; @@ -43,6 +45,7 @@ interface Props extends ViewProps { onLoaded?: () => void; isFirstSection?: boolean; onItemFocus?: (item: BaseItemDto) => void; + parentId?: string; } // TV-specific ItemCardText with larger fonts @@ -77,6 +80,54 @@ const TVItemCardText: React.FC<{ item: BaseItemDto }> = ({ item }) => { ); }; +// TV-specific "See All" card for end of lists +const TVSeeAllCard: React.FC<{ + onPress: () => void; + orientation: "horizontal" | "vertical"; + disabled?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}> = ({ onPress, orientation, disabled, onFocus, onBlur }) => { + const { t } = useTranslation(); + const width = + orientation === "horizontal" ? TV_LANDSCAPE_WIDTH : TV_POSTER_WIDTH; + const aspectRatio = orientation === "horizontal" ? 16 / 9 : 10 / 15; + + return ( + + + + + + {t("common.seeAll", { defaultValue: "See all" })} + + + + + ); +}; + export const InfiniteScrollingCollectionList: React.FC = ({ title, orientation = "vertical", @@ -89,6 +140,7 @@ export const InfiniteScrollingCollectionList: React.FC = ({ onLoaded, isFirstSection = false, onItemFocus, + parentId, ...props }) => { const effectivePageSize = Math.max(1, pageSize); @@ -101,13 +153,30 @@ export const InfiniteScrollingCollectionList: React.FC = ({ const flatListRef = useRef>(null); const [focusedCount, setFocusedCount] = useState(0); const prevFocusedCount = useRef(0); + const scrollBackTimerRef = useRef | null>(null); - // When section loses all focus, scroll back to start + // When section loses all focus, scroll back to start (with debounce to avoid + // triggering during transient focus changes like infinite scroll loading) useEffect(() => { + // Clear any pending scroll-back timer + if (scrollBackTimerRef.current) { + clearTimeout(scrollBackTimerRef.current); + scrollBackTimerRef.current = null; + } + if (prevFocusedCount.current > 0 && focusedCount === 0) { - flatListRef.current?.scrollToOffset({ offset: 0, animated: true }); + // Debounce the scroll-back to avoid triggering during re-renders + scrollBackTimerRef.current = setTimeout(() => { + flatListRef.current?.scrollToOffset({ offset: 0, animated: true }); + }, 150); } prevFocusedCount.current = focusedCount; + + return () => { + if (scrollBackTimerRef.current) { + clearTimeout(scrollBackTimerRef.current); + } + }; }, [focusedCount]); const handleItemFocus = useCallback( @@ -122,6 +191,11 @@ export const InfiniteScrollingCollectionList: React.FC = ({ setFocusedCount((c) => Math.max(0, c - 1)); }, []); + // Focus handler for See All card (doesn't need item parameter) + const handleSeeAllFocus = useCallback(() => { + setFocusedCount((c) => c + 1); + }, []); + const { data, isLoading, @@ -189,6 +263,18 @@ export const InfiniteScrollingCollectionList: React.FC = ({ } }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + const handleSeeAllPress = useCallback(() => { + if (!parentId) return; + router.push({ + pathname: "/(auth)/(tabs)/(libraries)/[libraryId]", + params: { + libraryId: parentId, + sortBy: SortByOption.DateCreated, + sortOrder: SortOrderOption.Descending, + }, + } as any); + }, [router, parentId]); + const getItemLayout = useCallback( (_data: ArrayLike | null | undefined, index: number) => ({ length: itemWidth + ITEM_GAP, @@ -359,23 +445,41 @@ export const InfiniteScrollingCollectionList: React.FC = ({ windowSize={5} removeClippedSubviews={false} getItemLayout={getItemLayout} + maintainVisibleContentPosition={{ minIndexForVisible: 0 }} style={{ overflow: "visible" }} contentContainerStyle={{ paddingVertical: SCALE_PADDING, paddingHorizontal: SCALE_PADDING, }} ListFooterComponent={ - isFetchingNextPage ? ( - - - - ) : null + + {isFetchingNextPage && ( + + + + )} + {parentId && allItems.length > 0 && ( + + )} + } /> )}