diff --git a/app/(auth)/(tabs)/libraries/[libraryId].tsx b/app/(auth)/(tabs)/libraries/[libraryId].tsx index b0f69a54..b9315dec 100644 --- a/app/(auth)/(tabs)/libraries/[libraryId].tsx +++ b/app/(auth)/(tabs)/libraries/[libraryId].tsx @@ -11,6 +11,7 @@ import React, { } from "react"; import { FlatList, View } from "react-native"; +import { Text } from "@/components/common/Text"; import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; import { FilterButton } from "@/components/filters/FilterButton"; import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton"; @@ -37,8 +38,6 @@ import { getUserLibraryApi, } from "@jellyfin/sdk/lib/utils/api"; import { FlashList } from "@shopify/flash-list"; -import { opacity } from "react-native-reanimated/lib/typescript/reanimated2/Colors"; -import { Text } from "@/components/common/Text"; const MemoizedTouchableItemRouter = React.memo(TouchableItemRouter); diff --git a/app/(auth)/(tabs)/search/index.tsx b/app/(auth)/(tabs)/search/index.tsx index c216649d..750e9db8 100644 --- a/app/(auth)/(tabs)/search/index.tsx +++ b/app/(auth)/(tabs)/search/index.tsx @@ -167,6 +167,16 @@ export default function search() { enabled: debouncedSearch.length > 0, }); + const { data: actors, isFetching: l8 } = useQuery({ + queryKey: ["search", "actors", debouncedSearch], + queryFn: () => + searchFn({ + query: debouncedSearch, + types: ["Person"], + }), + enabled: debouncedSearch.length > 0, + }); + const { data: artists, isFetching: l4 } = useQuery({ queryKey: ["search", "artists", debouncedSearch], queryFn: () => @@ -205,13 +215,14 @@ export default function search() { movies?.length || episodes?.length || series?.length || - collections?.length + collections?.length || + actors?.length ); - }, [artists, episodes, albums, songs, movies, series, collections]); + }, [artists, episodes, albums, songs, movies, series, collections, actors]); const loading = useMemo(() => { - return l1 || l2 || l3 || l4 || l5 || l6 || l7; - }, [l1, l2, l3, l4, l5, l6, l7]); + return l1 || l2 || l3 || l4 || l5 || l6 || l7 || l8; + }, [l1, l2, l3, l4, l5, l6, l7, l8]); return ( <> @@ -327,6 +338,25 @@ export default function search() { /> )} /> + m.Id!)} + header="Actors" + renderItem={(data) => ( + + data={data} + renderItem={(item) => ( + + + + + )} + /> + )} + /> m.Id!)} header="Artists" diff --git a/app/(auth)/actors/[actorId].tsx b/app/(auth)/actors/[actorId].tsx new file mode 100644 index 00000000..8cb2322e --- /dev/null +++ b/app/(auth)/actors/[actorId].tsx @@ -0,0 +1,151 @@ +import { Bitrate } from "@/components/BitrateSelector"; +import { Loader } from "@/components/Loader"; +import { OverviewText } from "@/components/OverviewText"; +import { Ratings } from "@/components/Ratings"; +import { Text } from "@/components/common/Text"; +import { MoviesTitleHeader } from "@/components/movies/MoviesTitleHeader"; +import { SeriesTitleHeader } from "@/components/series/SeriesTitleHeader"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { useSettings } from "@/utils/atoms/settings"; +import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; +import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; +import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; +import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; +import { chromecastProfile } from "@/utils/profiles/chromecast"; +import ios from "@/utils/profiles/ios"; +import native from "@/utils/profiles/native"; +import old from "@/utils/profiles/old"; +import { getItemsApi, getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api"; +import { useQuery } from "@tanstack/react-query"; +import { Image } from "expo-image"; +import { useLocalSearchParams } from "expo-router"; +import { useAtom } from "jotai"; +import { useCallback, useMemo, useState } from "react"; +import { View } from "react-native"; +import { useCastDevice } from "react-native-google-cast"; +import { ParallaxScrollView } from "../../../components/ParallaxPage"; +import { InfiniteHorizontalScroll } from "@/components/common/InfiniteHorrizontalScroll"; +import { TouchableItemRouter } from "@/components/common/TouchableItemRouter"; +import MoviePoster from "@/components/posters/MoviePoster"; +import { ItemCardText } from "@/components/ItemCardText"; +import { BaseItemDtoQueryResult } from "@jellyfin/sdk/lib/generated-client/models"; + +const page: React.FC = () => { + const local = useLocalSearchParams(); + const { actorId } = local as { actorId: string }; + + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + + const { data: item, isLoading: l1 } = useQuery({ + queryKey: ["item", actorId], + queryFn: async () => + await getUserItemData({ + api, + userId: user?.Id, + itemId: actorId, + }), + enabled: !!actorId && !!api, + staleTime: 60, + }); + + const fetchItems = useCallback( + async ({ + pageParam, + }: { + pageParam: number; + }): Promise => { + if (!api || !user?.Id) return null; + + const response = await getItemsApi(api).getItems({ + userId: user.Id, + personIds: [actorId], + startIndex: pageParam, + limit: 8, + sortOrder: ["Descending", "Descending", "Ascending"], + includeItemTypes: ["Movie", "Series"], + recursive: true, + fields: [ + "ParentId", + "PrimaryImageAspectRatio", + "ParentId", + "PrimaryImageAspectRatio", + ], + sortBy: ["PremiereDate", "ProductionYear", "SortName"], + collapseBoxSetItems: false, + }); + + return response.data; + }, + [api, user?.Id, actorId] + ); + + const backdropUrl = useMemo( + () => + getBackdropUrl({ + api, + item, + quality: 90, + width: 1000, + }), + [item] + ); + + if (l1) + return ( + + + + ); + + if (!item?.Id || !backdropUrl) return null; + + return ( + + } + > + + + + + + + + Appeared In + + ( + + + + + + + )} + queryFn={fetchItems} + queryKey={["actor", "movies", actorId]} + /> + + + + ); +}; + +export default page; diff --git a/app/_layout.tsx b/app/_layout.tsx index 6ff7e994..0f78cf87 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -118,6 +118,13 @@ function Layout() { headerShown: false, }} /> + = ({ text, ...props }) => { - const [limit, setLimit] = useState(LIMIT); +export const OverviewText: React.FC = ({ + text, + characterLimit = 140, + ...props +}) => { + const [limit, setLimit] = useState(characterLimit); if (!text) return null; - if (text.length > LIMIT) + if (text.length > characterLimit) return ( - setLimit((prev) => (prev === LIMIT ? text.length : LIMIT)) + setLimit((prev) => + prev === characterLimit ? text.length : characterLimit + ) } + {...props} > {tc(text, limit)} - {limit === LIMIT ? "Show more" : "Show less"} + {limit === characterLimit ? "Show more" : "Show less"} diff --git a/components/common/InfiniteHorrizontalScroll.tsx b/components/common/InfiniteHorrizontalScroll.tsx index 2b5d27cd..e8281d0e 100644 --- a/components/common/InfiniteHorrizontalScroll.tsx +++ b/components/common/InfiniteHorrizontalScroll.tsx @@ -17,7 +17,7 @@ import { Loader } from "../Loader"; import { Text } from "./Text"; interface HorizontalScrollProps - extends Omit, "renderItem" | "data"> { + extends Omit, "renderItem" | "data" | "style"> { queryFn: ({ pageParam, }: { diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index 516cd174..f1a36760 100644 --- a/components/common/TouchableItemRouter.tsx +++ b/components/common/TouchableItemRouter.tsx @@ -45,6 +45,10 @@ export const TouchableItemRouter: React.FC> = ({ router.push(`/artists/${item.Id}/page`); return; } + if (item.Type === "Person") { + router.push(`/actors/${item.Id}`); + return; + } if (item.Type === "BoxSet") { router.push(`/collections/${item.Id}`); diff --git a/components/movies/MoviesTitleHeader.tsx b/components/movies/MoviesTitleHeader.tsx index 71a3f4af..771cdb01 100644 --- a/components/movies/MoviesTitleHeader.tsx +++ b/components/movies/MoviesTitleHeader.tsx @@ -10,12 +10,8 @@ interface Props extends ViewProps { export const MoviesTitleHeader: React.FC = ({ item, ...props }) => { const router = useRouter(); return ( - <> - - - {item?.Name} - - - + + {item?.Name} + ); }; diff --git a/components/series/CastAndCrew.tsx b/components/series/CastAndCrew.tsx index 2de035fb..ab041bd7 100644 --- a/components/series/CastAndCrew.tsx +++ b/components/series/CastAndCrew.tsx @@ -28,10 +28,7 @@ export const CastAndCrew = ({ item }: { item: BaseItemDto }) => { renderItem={(item, index) => ( { - if (settings?.searchEngine === "Marlin") - router.push(`/search?q=${item.Name}&prev=${pathname}`); - else - Linking.openURL(`https://www.google.com/search?q=${item.Name}`); + router.push(`/actors/${item.Id}`); }} key={item.Id} className="flex flex-col w-32"