diff --git a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx index 9a2239f6..cdb1a181 100644 --- a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx +++ b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx @@ -12,11 +12,16 @@ import { import { FlashList } from "@shopify/flash-list"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; -import { useLocalSearchParams, useNavigation } from "expo-router"; +import { + useFocusEffect, + useLocalSearchParams, + useNavigation, +} from "expo-router"; import { useAtom } from "jotai"; -import React, { useCallback, useEffect, useMemo } from "react"; +import React, { useCallback, useEffect, useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; import { + BackHandler, FlatList, Platform, ScrollView, @@ -80,8 +85,9 @@ const Page = () => { sortBy?: string; sortOrder?: string; filterBy?: string; + fromSeeAll?: string; }; - const { libraryId } = searchParams; + const { libraryId, fromSeeAll } = searchParams; const typography = useScaledTVTypography(); const posterSizes = useScaledTVPosterSizes(); @@ -112,6 +118,22 @@ const Page = () => { const { t } = useTranslation(); const router = useRouter(); const { showOptions } = useTVOptionModal(); + + // When this library detail was opened from the home "See All" button, its + // libraries stack is just [detail], so the default TV Back would exit to home. + // Intercept Back (scoped to while this screen is focused via useFocusEffect) and + // route to the library list instead, so the user can switch libraries. Normal + // entries from the list keep their native pop-to-list behavior. + useFocusEffect( + useCallback(() => { + if (!Platform.isTV || fromSeeAll !== "true") return; + const sub = BackHandler.addEventListener("hardwareBackPress", () => { + router.replace("/(auth)/(tabs)/(libraries)"); + return true; + }); + return () => sub.remove(); + }, [fromSeeAll, router]), + ); const { showItemActions } = useTVItemActionModal(); // TV Filter queries @@ -269,6 +291,23 @@ const Page = () => { }); }, [library]); + // If this See-All detail was deep-linked on top of the libraries index, collapse + // the libraries stack to just this screen. Otherwise the stack is [index, detail], + // which the native bottom tab reliably auto-pops back to the index (the detail + // "bounces" to the library list ~0.5s after opening). With [detail] alone it stays + // put, and Back is handled explicitly by the fromSeeAll interceptor above. + const didCollapseRef = useRef(false); + useEffect(() => { + if (!Platform.isTV || fromSeeAll !== "true" || didCollapseRef.current) + return; + const state = navigation.getState(); + if (state?.routes && state.routes.length > 1) { + didCollapseRef.current = true; + const top = state.routes[state.routes.length - 1]; + navigation.reset({ index: 0, routes: [top] } as any); + } + }, [navigation, fromSeeAll]); + const fetchItems = useCallback( async ({ pageParam, diff --git a/components/home/InfiniteScrollingCollectionList.tv.tsx b/components/home/InfiniteScrollingCollectionList.tv.tsx index 346e9c0d..6a25535c 100644 --- a/components/home/InfiniteScrollingCollectionList.tv.tsx +++ b/components/home/InfiniteScrollingCollectionList.tv.tsx @@ -201,16 +201,18 @@ export const InfiniteScrollingCollectionList: React.FC = ({ const handleSeeAllPress = useCallback(() => { if (!parentId) return; - // Use the relative "/[libraryId]" pathname (same as getItemNavigation) so the - // library detail is pushed within the current tab's stack. The fully-qualified - // "/(auth)/(tabs)/(libraries)/[libraryId]" path is a cross-tab navigation that - // only switches to the libraries tab and drops the nested screen push. + // Navigate into the library detail (lives in the libraries tab) sorted by most + // recently added. The `fromSeeAll` flag tells the detail page to (a) collapse + // the libraries stack so the native tab can't auto-pop it back to the list, and + // (b) intercept Back to route to the library list so the user can switch + // libraries. See app/(auth)/(tabs)/(libraries)/[libraryId].tsx. router.push({ pathname: "/[libraryId]", params: { libraryId: parentId, sortBy: SortByOption.DateCreated, sortOrder: SortOrderOption.Descending, + fromSeeAll: "true", }, } as any); }, [router, parentId]);