From eeb4ef30082fe9f287369f98513abe0c26bf4063 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 19 Jan 2026 20:01:00 +0100 Subject: [PATCH] feat(tv): split actor filmography into movies and series sections --- components/persons/TVActorPage.tsx | 191 +++++++++++++++++++++-------- 1 file changed, 137 insertions(+), 54 deletions(-) diff --git a/components/persons/TVActorPage.tsx b/components/persons/TVActorPage.tsx index 879106c9..104c3d18 100644 --- a/components/persons/TVActorPage.tsx +++ b/components/persons/TVActorPage.tsx @@ -107,10 +107,7 @@ export const TVActorPage: React.FC = ({ personId }) => { const [user] = useAtom(userAtom); // Track which filmography item is currently focused for dynamic backdrop - const [focusedIndex, setFocusedIndex] = useState(0); - - // FlatList ref for scrolling back - const filmographyListRef = useRef>(null); + const [focusedItem, setFocusedItem] = useState(null); // Fetch actor details const { data: item, isLoading: isLoadingActor } = useQuery({ @@ -125,9 +122,9 @@ export const TVActorPage: React.FC = ({ personId }) => { staleTime: 60, }); - // Fetch filmography - const { data: filmography = [], isLoading: isLoadingFilmography } = useQuery({ - queryKey: ["actor", "filmography", personId], + // Fetch movies + const { data: movies = [], isLoading: isLoadingMovies } = useQuery({ + queryKey: ["actor", "movies", personId], queryFn: async () => { if (!api || !user?.Id) return []; @@ -137,7 +134,32 @@ export const TVActorPage: React.FC = ({ personId }) => { startIndex: 0, limit: 20, sortOrder: ["Descending", "Descending", "Ascending"], - includeItemTypes: ["Movie", "Series"], + includeItemTypes: ["Movie"], + recursive: true, + fields: ["ParentId", "PrimaryImageAspectRatio"], + sortBy: ["PremiereDate", "ProductionYear", "SortName"], + collapseBoxSetItems: false, + }); + + return response.data.Items || []; + }, + enabled: !!personId && !!api && !!user?.Id, + staleTime: 60, + }); + + // Fetch series + const { data: series = [], isLoading: isLoadingSeries } = useQuery({ + queryKey: ["actor", "series", personId], + queryFn: async () => { + if (!api || !user?.Id) return []; + + const response = await getItemsApi(api).getItems({ + userId: user.Id, + personIds: [personId], + startIndex: 0, + limit: 20, + sortOrder: ["Descending", "Descending", "Ascending"], + includeItemTypes: ["Series"], recursive: true, fields: ["ParentId", "PrimaryImageAspectRatio"], sortBy: ["PremiereDate", "ProductionYear", "SortName"], @@ -153,15 +175,16 @@ export const TVActorPage: React.FC = ({ personId }) => { // Get backdrop URL from the currently focused filmography item // Changes dynamically as user navigates through the list const backdropUrl = useMemo(() => { - if (filmography.length === 0) return null; - const focusedItem = filmography[focusedIndex] ?? filmography[0]; + // Use focused item if available, otherwise fall back to first movie or series + const itemForBackdrop = focusedItem ?? movies[0] ?? series[0]; + if (!itemForBackdrop) return null; return getBackdropUrl({ api, - item: focusedItem, + item: itemForBackdrop, quality: 90, width: 1920, }); - }, [api, filmography, focusedIndex]); + }, [api, focusedItem, movies, series]); // Crossfade animation for backdrop transitions // Use two alternating layers for smooth crossfade @@ -261,12 +284,15 @@ export const TVActorPage: React.FC = ({ personId }) => { // Render filmography item const renderFilmographyItem = useCallback( - ({ item: filmItem, index }: { item: BaseItemDto; index: number }) => ( + ( + { item: filmItem, index }: { item: BaseItemDto; index: number }, + isFirstSection: boolean, + ) => ( handleItemPress(filmItem)} - onFocus={() => setFocusedIndex(index)} - hasTVPreferredFocus={index === 0} + onFocus={() => setFocusedItem(filmItem)} + hasTVPreferredFocus={isFirstSection && index === 0} > @@ -469,21 +495,10 @@ export const TVActorPage: React.FC = ({ personId }) => { - {/* Filmography section */} + {/* Filmography sections */} - - {t("item_card.appeared_in")} - - - {isLoadingFilmography ? ( + {/* Movies Section */} + {isLoadingMovies ? ( = ({ personId }) => { > - ) : filmography.length === 0 ? ( - 0 && ( + + + {t("item_card.movies")} + + filmItem.Id!} + renderItem={(props) => renderFilmographyItem(props, true)} + showsHorizontalScrollIndicator={false} + initialNumToRender={6} + maxToRenderPerBatch={4} + windowSize={5} + removeClippedSubviews={false} + getItemLayout={getItemLayout} + style={{ overflow: "visible" }} + contentContainerStyle={{ + paddingVertical: SCALE_PADDING, + paddingHorizontal: SCALE_PADDING, + }} + /> + + ) + )} + + {/* Series Section */} + {isLoadingSeries ? ( + - {t("common.no_results")} - + + ) : ( - filmItem.Id!} - renderItem={renderFilmographyItem} - showsHorizontalScrollIndicator={false} - initialNumToRender={6} - maxToRenderPerBatch={4} - windowSize={5} - removeClippedSubviews={false} - getItemLayout={getItemLayout} - style={{ overflow: "visible" }} - contentContainerStyle={{ - paddingVertical: SCALE_PADDING, - paddingHorizontal: SCALE_PADDING, - }} - /> + series.length > 0 && ( + + + {t("item_card.shows")} + + filmItem.Id!} + renderItem={(props) => + renderFilmographyItem(props, movies.length === 0) + } + showsHorizontalScrollIndicator={false} + initialNumToRender={6} + maxToRenderPerBatch={4} + windowSize={5} + removeClippedSubviews={false} + getItemLayout={getItemLayout} + style={{ overflow: "visible" }} + contentContainerStyle={{ + paddingVertical: SCALE_PADDING, + paddingHorizontal: SCALE_PADDING, + }} + /> + + ) )} + + {/* Empty state - only show if both sections are empty and not loading */} + {!isLoadingMovies && + !isLoadingSeries && + movies.length === 0 && + series.length === 0 && ( + + {t("common.no_results")} + + )}