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 && (
+
+ )}
+
}
/>
)}