diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/[personId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/[personId].tsx
deleted file mode 100644
index 5a930982..00000000
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/[personId].tsx
+++ /dev/null
@@ -1,247 +0,0 @@
-import {
- router,
- useLocalSearchParams,
- useNavigation,
- useSegments,
-} from "expo-router";
-import React, {
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from "react";
-import { TouchableOpacity, View } from "react-native";
-import { useQuery } from "@tanstack/react-query";
-import { useJellyseerr } from "@/hooks/useJellyseerr";
-import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { ParallaxScrollView } from "@/components/ParallaxPage";
-import { Text } from "@/components/common/Text";
-import { Animated } from "react-native";
-import { Image } from "expo-image";
-import { OverviewText } from "@/components/OverviewText";
-import { orderBy } from "lodash";
-import { FlashList } from "@shopify/flash-list";
-import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
-import Poster from "@/components/posters/Poster";
-import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon";
-
-const ANIMATION_ENTER = 250;
-const ANIMATION_EXIT = 250;
-const BACKDROP_DURATION = 5000;
-
-export default function page() {
- const insets = useSafeAreaInsets();
- const local = useLocalSearchParams();
- const segments = useSegments();
- const { jellyseerrApi, jellyseerrUser } = useJellyseerr();
-
- const { personId } = local as { personId: string };
- const from = segments[2];
-
- const [currentIndex, setCurrentIndex] = useState(0);
- const fadeAnim = useRef(new Animated.Value(0)).current;
-
- const { data, isLoading, isFetching } = useQuery({
- queryKey: ["jellyseerr", "person", personId],
- queryFn: async () => ({
- details: await jellyseerrApi?.personDetails(personId),
- combinedCredits: await jellyseerrApi?.personCombinedCredits(personId),
- }),
- enabled: !!jellyseerrApi && !!personId,
- });
-
- const locale = useMemo(() => {
- return jellyseerrUser?.settings?.locale || "en";
- }, [jellyseerrUser]);
-
- const region = useMemo(
- () => jellyseerrUser?.settings?.region || "US",
- [jellyseerrUser]
- );
-
- const castedRoles: PersonCreditCast[] = useMemo(
- () =>
- orderBy(
- data?.combinedCredits?.cast,
- ["voteCount", "voteAverage"],
- "desc"
- ),
- [data?.combinedCredits]
- );
-
- const backdrops = useMemo(
- () => castedRoles.map((c) => c.backdropPath),
- [data?.combinedCredits]
- );
-
- const enterAnimation = useCallback(
- () =>
- Animated.timing(fadeAnim, {
- toValue: 1,
- duration: ANIMATION_ENTER,
- useNativeDriver: true,
- }),
- [fadeAnim]
- );
-
- const exitAnimation = useCallback(
- () =>
- Animated.timing(fadeAnim, {
- toValue: 0,
- duration: ANIMATION_EXIT,
- useNativeDriver: true,
- }),
- [fadeAnim]
- );
-
- useEffect(() => {
- if (backdrops?.length) {
- enterAnimation().start();
- const intervalId = setInterval(() => {
- exitAnimation().start((end) => {
- if (end.finished)
- setCurrentIndex((prevIndex) => (prevIndex + 1) % backdrops?.length);
- });
- }, BACKDROP_DURATION);
-
- return () => clearInterval(intervalId);
- }
- }, [backdrops, enterAnimation, exitAnimation, setCurrentIndex, currentIndex]);
-
- const viewDetails = (credit: PersonCreditCast) => {
- router.push({
- //@ts-ignore
- pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
- //@ts-ignore
- params: {
- ...credit,
- mediaTitle: credit.title,
- releaseYear: new Date(credit.releaseDate).getFullYear(),
- canRequest: "false",
- posterSrc: jellyseerrApi?.imageProxy(
- credit.posterPath,
- "w300_and_h450_face"
- ),
- },
- });
- };
-
- return (
-
-
- }
- logo={
-
- }
- >
-
-
-
-
- {data?.details?.name}
-
-
- Born{" "}
- {new Date(data?.details?.birthday!!).toLocaleDateString(
- `${locale}-${region}`,
- {
- year: "numeric",
- month: "long",
- day: "numeric",
- }
- )}{" "}
- | {data?.details?.placeOfBirth}
-
-
-
-
-
-
-
-
- No results
-
-
- }
- contentInsetAdjustmentBehavior="automatic"
- ListHeaderComponent={
- Appearances
- }
- renderItem={({ item }) => (
- viewDetails(item)}
- >
-
-
- {/*{item.title}*/}
- {item.character && (
-
- as {item.character}
-
- )}
-
- )}
- keyExtractor={(item) => item.id.toString()}
- estimatedItemSize={255}
- numColumns={3}
- contentContainerStyle={{ paddingBottom: 24 }}
- ItemSeparatorComponent={() => }
- />
-
-
-
-
- );
-}
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx
new file mode 100644
index 00000000..02762a51
--- /dev/null
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/company/[companyId].tsx
@@ -0,0 +1,136 @@
+import {router, useLocalSearchParams, useSegments,} from "expo-router";
+import React, {useMemo,} from "react";
+import {TouchableOpacity} from "react-native";
+import {useInfiniteQuery} from "@tanstack/react-query";
+import {Endpoints, useJellyseerr} from "@/hooks/useJellyseerr";
+import {Text} from "@/components/common/Text";
+import {Image} from "expo-image";
+import Poster from "@/components/posters/Poster";
+import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon";
+import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover";
+import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
+import {MovieResult, Results, TvResult} from "@/utils/jellyseerr/server/models/Search";
+import {COMPANY_LOGO_IMAGE_FILTER} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
+import {uniqBy} from "lodash";
+
+export default function page() {
+ const local = useLocalSearchParams();
+ const segments = useSegments();
+ const {jellyseerrApi} = useJellyseerr();
+
+ const from = segments[2];
+ const {companyId, name, image, type} = local as unknown as {
+ companyId: string,
+ name: string,
+ image: string,
+ type: DiscoverSliderType
+ };
+
+ const {data, fetchNextPage, hasNextPage} = useInfiniteQuery({
+ queryKey: ["jellyseerr", "company", type, companyId],
+ queryFn: async ({pageParam}) => {
+ let params: any = {
+ page: Number(pageParam),
+ };
+
+ return jellyseerrApi?.discover(
+ (
+ type == DiscoverSliderType.NETWORKS
+ ? Endpoints.DISCOVER_TV_NETWORK
+ : Endpoints.DISCOVER_MOVIES_STUDIO
+ ) + `/${companyId}`,
+ params
+ )
+ },
+ enabled: !!jellyseerrApi && !!companyId,
+ initialPageParam: 1,
+ getNextPageParam: (lastPage, pages) =>
+ (lastPage?.page || pages?.findLast((p) => p?.results.length)?.page || 1) +
+ 1,
+ staleTime: 0,
+ });
+
+ const flatData = useMemo(
+ () => uniqBy(data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results ?? []), "id")?? [],
+ [data]
+ );
+
+ const backdrops = useMemo(
+ () => jellyseerrApi
+ ? flatData.map((r) => jellyseerrApi.imageProxy((r as TvResult | MovieResult).backdropPath, "w1920_and_h800_multi_faces"))
+ : [],
+ [jellyseerrApi, flatData]
+ );
+
+ const viewDetails = (result: Results) => {
+ router.push({
+ //@ts-ignore
+ pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
+ //@ts-ignore
+ params: {
+ ...result,
+ mediaTitle: getName(result),
+ releaseYear: getYear(result),
+ canRequest: "false",
+ posterSrc: jellyseerrApi?.imageProxy(
+ (result as MovieResult | TvResult).posterPath,
+ "w300_and_h450_face"
+ ),
+ },
+ });
+ };
+
+ const getName = (result: Results) => {
+ return (result as TvResult).name || (result as MovieResult).title
+ }
+
+ const getYear = (result: Results) => {
+ return new Date((result as TvResult).firstAirDate || (result as MovieResult).releaseDate).getFullYear()
+ }
+
+ return (
+ item.id.toString()}
+ onEndReached={() => {
+ if (hasNextPage) {
+ fetchNextPage()
+ }
+ }}
+ logo={
+
+ }
+ renderItem={(item, index) => (
+ viewDetails(item)}
+ >
+
+
+ {getName(item)}
+ {getYear(item)}
+
+ )}
+ />
+ );
+}
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/genre/[genreId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/genre/[genreId].tsx
new file mode 100644
index 00000000..34a4fc7b
--- /dev/null
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/genre/[genreId].tsx
@@ -0,0 +1,128 @@
+import {router, useLocalSearchParams, useSegments,} from "expo-router";
+import React, {useMemo,} from "react";
+import {TouchableOpacity} from "react-native";
+import {useInfiniteQuery} from "@tanstack/react-query";
+import {Endpoints, useJellyseerr} from "@/hooks/useJellyseerr";
+import {Text} from "@/components/common/Text";
+import Poster from "@/components/posters/Poster";
+import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon";
+import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover";
+import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
+import {MovieResult, Results, TvResult} from "@/utils/jellyseerr/server/models/Search";
+import {uniqBy} from "lodash";
+import {textShadowStyle} from "@/components/jellyseerr/discover/GenericSlideCard";
+
+export default function page() {
+ const local = useLocalSearchParams();
+ const segments = useSegments();
+ const {jellyseerrApi} = useJellyseerr();
+
+ const from = segments[2];
+ const {genreId, name, type} = local as unknown as {
+ genreId: string,
+ name: string,
+ type: DiscoverSliderType
+ };
+
+ const {data, fetchNextPage, hasNextPage} = useInfiniteQuery({
+ queryKey: ["jellyseerr", "company", type, genreId],
+ queryFn: async ({pageParam}) => {
+ let params: any = {
+ page: Number(pageParam),
+ genre: genreId
+ };
+
+ return jellyseerrApi?.discover(
+ type == DiscoverSliderType.MOVIE_GENRES
+ ? Endpoints.DISCOVER_MOVIES
+ : Endpoints.DISCOVER_TV,
+ params
+ )
+ },
+ enabled: !!jellyseerrApi && !!genreId,
+ initialPageParam: 1,
+ getNextPageParam: (lastPage, pages) =>
+ (lastPage?.page || pages?.findLast((p) => p?.results.length)?.page || 1) +
+ 1,
+ staleTime: 0,
+ });
+
+ const flatData = useMemo(
+ () => uniqBy(data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results ?? []), "id")?? [],
+ [data]
+ );
+
+ const backdrops = useMemo(
+ () => jellyseerrApi
+ ? flatData.map((r) => jellyseerrApi.imageProxy((r as TvResult | MovieResult).backdropPath, "w1920_and_h800_multi_faces"))
+ : [],
+ [jellyseerrApi, flatData]
+ );
+
+ const viewDetails = (result: Results) => {
+ router.push({
+ //@ts-ignore
+ pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
+ //@ts-ignore
+ params: {
+ ...result,
+ mediaTitle: getName(result),
+ releaseYear: getYear(result),
+ canRequest: "false",
+ posterSrc: jellyseerrApi?.imageProxy(
+ (result as MovieResult | TvResult).posterPath,
+ "w300_and_h450_face"
+ ),
+ },
+ });
+ };
+
+ const getName = (result: Results) => {
+ return (result as TvResult).name || (result as MovieResult).title
+ }
+
+ const getYear = (result: Results) => {
+ return new Date((result as TvResult).firstAirDate || (result as MovieResult).releaseDate).getFullYear()
+ }
+
+ return (
+ item.id.toString()}
+ onEndReached={() => {
+ if (hasNextPage) {
+ fetchNextPage()
+ }
+ }}
+ logo={
+
+ {name}
+
+ }
+ renderItem={(item, index) => (
+ viewDetails(item)}
+ >
+
+
+ {getName(item)}
+ {getYear(item)}
+
+ )}
+ />
+ );
+}
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/person/[personId].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/person/[personId].tsx
new file mode 100644
index 00000000..84f59f4b
--- /dev/null
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/person/[personId].tsx
@@ -0,0 +1,151 @@
+import {
+ router,
+ useLocalSearchParams,
+ useSegments,
+} from "expo-router";
+import React, { useMemo } from "react";
+import { TouchableOpacity } from "react-native";
+import { useQuery } from "@tanstack/react-query";
+import { useJellyseerr } from "@/hooks/useJellyseerr";
+import { Text } from "@/components/common/Text";
+import { Image } from "expo-image";
+import { OverviewText } from "@/components/OverviewText";
+import { orderBy } from "lodash";
+import { PersonCreditCast } from "@/utils/jellyseerr/server/models/Person";
+import Poster from "@/components/posters/Poster";
+import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon";
+import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
+
+export default function page() {
+ const local = useLocalSearchParams();
+ const segments = useSegments();
+ const { jellyseerrApi, jellyseerrUser } = useJellyseerr();
+
+ const { personId } = local as { personId: string };
+ const from = segments[2];
+
+ const { data, isLoading, isFetching } = useQuery({
+ queryKey: ["jellyseerr", "person", personId],
+ queryFn: async () => ({
+ details: await jellyseerrApi?.personDetails(personId),
+ combinedCredits: await jellyseerrApi?.personCombinedCredits(personId),
+ }),
+ enabled: !!jellyseerrApi && !!personId,
+ });
+
+ const locale = useMemo(() => {
+ return jellyseerrUser?.settings?.locale || "en";
+ }, [jellyseerrUser]);
+
+ const region = useMemo(
+ () => jellyseerrUser?.settings?.region || "US",
+ [jellyseerrUser]
+ );
+
+ const castedRoles: PersonCreditCast[] = useMemo(
+ () =>
+ orderBy(
+ data?.combinedCredits?.cast,
+ ["voteCount", "voteAverage"],
+ "desc"
+ ),
+ [data?.combinedCredits]
+ );
+ const backdrops = useMemo(
+ () => jellyseerrApi
+ ? castedRoles.map((c) => jellyseerrApi.imageProxy(c.backdropPath, "w1920_and_h800_multi_faces"))
+ : [],
+ [jellyseerrApi, data?.combinedCredits]
+ );
+
+ const viewDetails = (credit: PersonCreditCast) => {
+ router.push({
+ //@ts-ignore
+ pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
+ //@ts-ignore
+ params: {
+ ...credit,
+ mediaTitle: credit.title,
+ releaseYear: new Date(credit.releaseDate).getFullYear(),
+ canRequest: "false",
+ posterSrc: jellyseerrApi?.imageProxy(
+ credit.posterPath,
+ "w300_and_h450_face"
+ ),
+ },
+ });
+ };
+
+ return (
+ item.id.toString()}
+ logo={
+
+ }
+ HeaderContent={() => (
+ <>
+
+ {data?.details?.name}
+
+
+ Born{" "}
+ {new Date(data?.details?.birthday!!).toLocaleDateString(
+ `${locale}-${region}`,
+ {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ }
+ )}{" "}
+ | {data?.details?.placeOfBirth}
+
+ >
+ )}
+ MainContent={() => (
+
+ )}
+ renderItem={(item, index) => (
+ viewDetails(item)}
+ >
+
+
+ {item.character && (
+
+ as {item.character}
+
+ )}
+
+ )}
+ />
+ );
+}
diff --git a/app/(auth)/(tabs)/(search)/_layout.tsx b/app/(auth)/(tabs)/(search)/_layout.tsx
index 1119e2a4..1f6a3c8b 100644
--- a/app/(auth)/(tabs)/(search)/_layout.tsx
+++ b/app/(auth)/(tabs)/(search)/_layout.tsx
@@ -36,7 +36,9 @@ export default function SearchLayout() {
}}
/>
-
+
+
+
);
}
diff --git a/bun.lockb b/bun.lockb
index 3a1947ce..ce47c71e 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/components/ParallaxPage.tsx b/components/ParallaxPage.tsx
index daebca6b..5d7b28e0 100644
--- a/components/ParallaxPage.tsx
+++ b/components/ParallaxPage.tsx
@@ -1,6 +1,6 @@
import { LinearGradient } from "expo-linear-gradient";
import { type PropsWithChildren, type ReactElement } from "react";
-import { View, ViewProps } from "react-native";
+import {NativeScrollEvent, NativeSyntheticEvent, View, ViewProps} from "react-native";
import Animated, {
interpolate,
useAnimatedRef,
@@ -13,6 +13,7 @@ interface Props extends ViewProps {
logo?: ReactElement;
episodePoster?: ReactElement;
headerHeight?: number;
+ onEndReached?: (() => void) | null | undefined;
}
export const ParallaxScrollView: React.FC> = ({
@@ -21,6 +22,7 @@ export const ParallaxScrollView: React.FC> = ({
episodePoster,
headerHeight = 400,
logo,
+ onEndReached,
...props
}: Props) => {
const scrollRef = useAnimatedRef();
@@ -47,6 +49,11 @@ export const ParallaxScrollView: React.FC> = ({
};
});
+
+ function isCloseToBottom({layoutMeasurement, contentOffset, contentSize}: NativeScrollEvent) {
+ return layoutMeasurement.height + contentOffset.y >= contentSize.height - 20;
+ }
+
return (
> = ({
}}
ref={scrollRef}
scrollEventThrottle={16}
+ onScroll={e => {
+ if (isCloseToBottom(e.nativeEvent))
+ onEndReached?.()
+ }}
>
{logo && (
= ({ searchQuery }) => {
if (!searchQuery.length)
return (
- {sortBy?.(
- jellyseerrDiscoverSettings?.filter((s) => s.enabled),
- "order"
- ).map((slide) => (
-
- ))}
+
);
diff --git a/components/jellyseerr/ParallaxSlideShow.tsx b/components/jellyseerr/ParallaxSlideShow.tsx
new file mode 100644
index 00000000..1e3d3f4f
--- /dev/null
+++ b/components/jellyseerr/ParallaxSlideShow.tsx
@@ -0,0 +1,155 @@
+import React, {
+ PropsWithChildren,
+ useCallback,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+import {Dimensions, View, ViewProps} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { ParallaxScrollView } from "@/components/ParallaxPage";
+import { Text } from "@/components/common/Text";
+import { Animated } from "react-native";
+import { FlashList } from "@shopify/flash-list";
+import {useFocusEffect} from "expo-router";
+
+const ANIMATION_ENTER = 250;
+const ANIMATION_EXIT = 250;
+const BACKDROP_DURATION = 5000;
+
+type Render = React.ComponentType
+ | React.ReactElement
+ | null
+ | undefined;
+
+interface Props {
+ data: T[]
+ images: string[];
+ logo?: React.ReactElement;
+ HeaderContent?: () => React.ReactElement;
+ MainContent?: () => React.ReactElement;
+ listHeader: string;
+ renderItem: (item: T, index: number) => Render;
+ keyExtractor: (item: T) => string;
+ onEndReached?: (() => void) | null | undefined;
+}
+
+const ParallaxSlideShow = ({
+ data,
+ images,
+ logo,
+ HeaderContent,
+ MainContent,
+ listHeader,
+ renderItem,
+ keyExtractor,
+ onEndReached,
+ ...props
+}: PropsWithChildren & ViewProps>
+) => {
+ const insets = useSafeAreaInsets();
+
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const [onEnd, setOnEnd] = useState(true);
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+
+ const enterAnimation = useCallback(
+ () =>
+ Animated.timing(fadeAnim, {
+ toValue: 1,
+ duration: ANIMATION_ENTER,
+ useNativeDriver: true,
+ }),
+ [fadeAnim]
+ );
+
+ const exitAnimation = useCallback(
+ () =>
+ Animated.timing(fadeAnim, {
+ toValue: 0,
+ duration: ANIMATION_EXIT,
+ useNativeDriver: true,
+ }),
+ [fadeAnim]
+ );
+
+ useEffect(() => {
+ if (images?.length) {
+ enterAnimation().start();
+ const intervalId = setInterval(() => {
+ exitAnimation().start((end) => {
+ if (end.finished)
+ setCurrentIndex((prevIndex) => (prevIndex + 1) % images?.length);
+ });
+ }, BACKDROP_DURATION);
+
+ return () => clearInterval(intervalId);
+ }
+ }, [images, enterAnimation, exitAnimation, setCurrentIndex, currentIndex]);
+
+ return (
+
+
+ }
+ logo={logo}
+ >
+
+
+
+ {HeaderContent && HeaderContent()}
+
+
+ {MainContent && MainContent()}
+
+
+
+ No results
+
+
+ }
+ contentInsetAdjustmentBehavior="automatic"
+ ListHeaderComponent={
+ {listHeader}
+ }
+ nestedScrollEnabled
+ showsVerticalScrollIndicator={false}
+ //@ts-ignore
+ renderItem={({ item, index}) => renderItem(item, index)}
+ keyExtractor={keyExtractor}
+ numColumns={3}
+ estimatedItemSize={214}
+ ItemSeparatorComponent={() => }
+ />
+
+
+
+
+ );
+}
+
+export default ParallaxSlideShow;
\ No newline at end of file
diff --git a/components/jellyseerr/PersonPoster.tsx b/components/jellyseerr/PersonPoster.tsx
index 57ff9f58..6e7d9aa6 100644
--- a/components/jellyseerr/PersonPoster.tsx
+++ b/components/jellyseerr/PersonPoster.tsx
@@ -26,7 +26,7 @@ const PersonPoster: React.FC = ({
if (from === "(home)" || from === "(search)" || from === "(libraries)")
return (
- router.push(`/(auth)/(tabs)/${from}/jellyseerr/${id}`)}>
+ router.push(`/(auth)/(tabs)/${from}/jellyseerr/person/${id}`)}>
= ({ slide, data, ...props }) => {
+ const segments = useSegments();
+ const { jellyseerrApi } = useJellyseerr();
+ const from = segments[2];
+
+ const navigate = useCallback(({id, image, name}: Network | Studio) => router.push({
+ pathname: `/(auth)/(tabs)/${from}/jellyseerr/company/${id}`,
+ params: {id, image, name, type: slide.type }
+ }), [slide]);
+
+ return (
+ item.id.toString()}
+ renderItem={(item, index) => (
+ navigate(item)}>
+
+
+ )}
+ />
+ );
+};
+
+export default CompanySlide;
diff --git a/components/jellyseerr/discover/Discover.tsx b/components/jellyseerr/discover/Discover.tsx
new file mode 100644
index 00000000..6270ad2b
--- /dev/null
+++ b/components/jellyseerr/discover/Discover.tsx
@@ -0,0 +1,47 @@
+import React, {useMemo} from "react";
+import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
+import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover";
+import {sortBy} from "lodash";
+import MovieTvSlide from "@/components/jellyseerr/discover/MovieTvSlide";
+import CompanySlide from "@/components/jellyseerr/discover/CompanySlide";
+import {View} from "react-native";
+import {networks} from "@/utils/jellyseerr/src/components/Discover/NetworkSlider";
+import {studios} from "@/utils/jellyseerr/src/components/Discover/StudioSlider";
+import GenreSlide from "@/components/jellyseerr/discover/GenreSlide";
+
+interface Props {
+ sliders?: DiscoverSlider[];
+}
+const Discover: React.FC = ({ sliders }) => {
+ if (!sliders)
+ return;
+
+ const sortedSliders = useMemo(
+ () => sortBy(sliders.filter((s) => s.enabled), 'order', 'asc'),
+ [sliders]
+ );
+
+ return (
+
+ {sortedSliders.map(slide => {
+ switch (slide.type) {
+ case DiscoverSliderType.NETWORKS:
+ return
+ case DiscoverSliderType.STUDIOS:
+ return
+ case DiscoverSliderType.MOVIE_GENRES:
+ case DiscoverSliderType.TV_GENRES:
+ return
+ case DiscoverSliderType.TRENDING:
+ case DiscoverSliderType.POPULAR_MOVIES:
+ case DiscoverSliderType.UPCOMING_MOVIES:
+ case DiscoverSliderType.POPULAR_TV:
+ case DiscoverSliderType.UPCOMING_TV:
+ return
+ }
+ })}
+
+ )
+};
+
+export default Discover;
diff --git a/components/jellyseerr/discover/GenericSlideCard.tsx b/components/jellyseerr/discover/GenericSlideCard.tsx
new file mode 100644
index 00000000..776d1424
--- /dev/null
+++ b/components/jellyseerr/discover/GenericSlideCard.tsx
@@ -0,0 +1,59 @@
+import React from "react";
+import {StyleSheet, View, ViewProps} from "react-native";
+import {Image, ImageContentFit} from "expo-image";
+import {Text} from "@/components/common/Text";
+import {LinearGradient} from "expo-linear-gradient";
+
+export const textShadowStyle = StyleSheet.create({
+ shadow: {
+ shadowColor: "#000",
+ shadowOffset: {
+ width: 1,
+ height: 1,
+ },
+ shadowOpacity: 1,
+ shadowRadius: .5,
+
+ elevation: 6,
+ }
+})
+
+const GenericSlideCard: React.FC<{id: string; url?: string, title?: string, colors?: string[], contentFit?: ImageContentFit} & ViewProps> = ({
+ id,
+ url,
+ title,
+ colors = ['#9333ea', 'transparent'],
+ contentFit = "contain",
+ ...props
+}) => (
+ <>
+
+
+
+ {title &&
+
+ {title}
+
+ }
+
+
+ >
+);
+
+export default GenericSlideCard;
\ No newline at end of file
diff --git a/components/jellyseerr/discover/GenreSlide.tsx b/components/jellyseerr/discover/GenreSlide.tsx
new file mode 100644
index 00000000..551ee2de
--- /dev/null
+++ b/components/jellyseerr/discover/GenreSlide.tsx
@@ -0,0 +1,56 @@
+import React, {useCallback} from "react";
+import {Endpoints, useJellyseerr,} from "@/hooks/useJellyseerr";
+import {TouchableOpacity, ViewProps} from "react-native";
+import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide";
+import GenericSlideCard from "@/components/jellyseerr/discover/GenericSlideCard";
+import {router, useSegments} from "expo-router";
+import {useQuery} from "@tanstack/react-query";
+import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover";
+import {genreColorMap} from "@/utils/jellyseerr/src/components/Discover/constants";
+import {GenreSliderItem} from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces";
+
+const GenreSlide: React.FC = ({ slide, ...props }) => {
+ const segments = useSegments();
+ const { jellyseerrApi } = useJellyseerr();
+ const from = segments[2];
+
+ const navigate = useCallback((genre: GenreSliderItem) => router.push({
+ pathname: `/(auth)/(tabs)/${from}/jellyseerr/genre/${genre.id}`,
+ params: {type: slide.type, name: genre.name}
+ }), [slide]);
+
+ const {data, isFetching, isLoading } = useQuery({
+ queryKey: ['jellyseerr', 'discover', slide.type, slide.id],
+ queryFn: async () => {
+ return jellyseerrApi?.getGenreSliders(
+ slide.type == DiscoverSliderType.MOVIE_GENRES
+ ? Endpoints.MOVIE
+ : Endpoints.TV
+ )
+ },
+ enabled: !!jellyseerrApi
+ })
+
+ return (
+ data && item.id.toString()}
+ renderItem={(item, index) => (
+ navigate(item)}>
+
+
+ )}
+ />
+ );
+};
+
+export default GenreSlide;
diff --git a/components/jellyseerr/DiscoverSlide.tsx b/components/jellyseerr/discover/MovieTvSlide.tsx
similarity index 58%
rename from components/jellyseerr/DiscoverSlide.tsx
rename to components/jellyseerr/discover/MovieTvSlide.tsx
index c7112def..723658c8 100644
--- a/components/jellyseerr/DiscoverSlide.tsx
+++ b/components/jellyseerr/discover/MovieTvSlide.tsx
@@ -1,5 +1,4 @@
-import React, { useMemo } from "react";
-import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
+import React, {useMemo} from "react";
import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
import {
DiscoverEndpoint,
@@ -9,17 +8,13 @@ import {
import { useInfiniteQuery } from "@tanstack/react-query";
import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
-import { Text } from "@/components/common/Text";
-import { FlashList } from "@shopify/flash-list";
-import { View } from "react-native";
+import Slide, {SlideProps} from "@/components/jellyseerr/discover/Slide";
+import {ViewProps} from "react-native";
-interface Props {
- slide: DiscoverSlider;
-}
-const DiscoverSlide: React.FC = ({ slide }) => {
+const MovieTvSlide: React.FC = ({ slide, ...props }) => {
const { jellyseerrApi } = useJellyseerr();
- const { data, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery({
+ const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ["jellyseerr", "discover", slide.id],
queryFn: async ({ pageParam }) => {
let endpoint: DiscoverEndpoint | undefined = undefined;
@@ -62,42 +57,28 @@ const DiscoverSlide: React.FC = ({ slide }) => {
});
const flatData = useMemo(
- () =>
- data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results),
+ () => data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results),
[data]
);
return (
flatData &&
flatData?.length > 0 && (
-
-
- {DiscoverSliderType[slide.type].toString().toTitle()}
-
- item!!.id.toString()}
- estimatedItemSize={250}
- data={flatData}
- onEndReachedThreshold={1}
- onEndReached={() => {
- if (hasNextPage) fetchNextPage();
- }}
- renderItem={({ item }) =>
- item ? (
-
- ) : (
- <>>
- )
- }
- />
-
+ item!!.id.toString()}
+ onEndReached={() => {
+ if (hasNextPage)
+ fetchNextPage()
+ }}
+ renderItem={(item) =>
+
+ }
+ />
)
);
};
-export default DiscoverSlide;
+export default MovieTvSlide;
diff --git a/components/jellyseerr/discover/Slide.tsx b/components/jellyseerr/discover/Slide.tsx
new file mode 100644
index 00000000..5a593b41
--- /dev/null
+++ b/components/jellyseerr/discover/Slide.tsx
@@ -0,0 +1,55 @@
+import React, {PropsWithChildren} from "react";
+import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
+import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover";
+import { Text } from "@/components/common/Text";
+import { FlashList } from "@shopify/flash-list";
+import {View, ViewProps} from "react-native";
+
+export interface SlideProps {
+ slide: DiscoverSlider;
+}
+
+interface Props extends SlideProps {
+ data: T[]
+ renderItem: (item: T, index: number) =>
+ | React.ComponentType
+ | React.ReactElement
+ | null
+ | undefined;
+ keyExtractor: (item: T) => string;
+ onEndReached?: (() => void) | null | undefined;
+}
+
+const Slide = ({
+ data,
+ slide,
+ renderItem,
+ keyExtractor,
+ onEndReached,
+ ...props
+}: PropsWithChildren & ViewProps>
+) => {
+ return (
+
+
+ {DiscoverSliderType[slide.type].toString().toTitle()}
+
+ item ? renderItem(item, index) : <>>}
+ />
+
+ );
+};
+
+export default Slide;
diff --git a/hooks/useJellyseerr.ts b/hooks/useJellyseerr.ts
index 815510fa..8d3eba30 100644
--- a/hooks/useJellyseerr.ts
+++ b/hooks/useJellyseerr.ts
@@ -33,6 +33,7 @@ import {
PersonDetails,
} from "@/utils/jellyseerr/server/models/Person";
import { useQueryClient } from "@tanstack/react-query";
+import {GenreSliderItem} from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces";
interface SearchParams {
query: string;
@@ -67,14 +68,20 @@ export enum Endpoints {
ISSUE = "/issue",
TV = "/tv",
SETTINGS = "/settings",
+ NETWORK = "/network",
+ STUDIO = "/studio",
+ GENRE_SLIDER = "/genreslider",
DISCOVER = "/discover",
DISCOVER_TRENDING = DISCOVER + "/trending",
DISCOVER_MOVIES = DISCOVER + "/movies",
DISCOVER_TV = DISCOVER + TV,
+ DISCOVER_TV_NETWORK = DISCOVER + TV + NETWORK,
+ DISCOVER_MOVIES_STUDIO = DISCOVER + `${MOVIE}s` + STUDIO,
AUTH_JELLYFIN = "/auth/jellyfin",
}
export type DiscoverEndpoint =
+ | Endpoints.DISCOVER_TV_NETWORK
| Endpoints.DISCOVER_TRENDING
| Endpoints.DISCOVER_MOVIES
| Endpoints.DISCOVER_TV;
@@ -181,7 +188,7 @@ export class JellyseerrApi {
}
async discover(
- endpoint: DiscoverEndpoint,
+ endpoint: DiscoverEndpoint | string,
params: any
): Promise {
return this.axios
@@ -189,6 +196,15 @@ export class JellyseerrApi {
.then(({ data }) => data);
}
+ async getGenreSliders(
+ endpoint: Endpoints.TV | Endpoints.MOVIE,
+ params: any = undefined
+ ): Promise {
+ return this.axios
+ ?.get(Endpoints.API_V1 + Endpoints.DISCOVER + Endpoints.GENRE_SLIDER + endpoint, { params })
+ .then(({ data }) => data);
+ }
+
async search(params: SearchParams): Promise {
const response = await this.axios?.get(
Endpoints.API_V1 + Endpoints.SEARCH,
@@ -268,7 +284,7 @@ export class JellyseerrApi {
imageProxy(
path?: string,
- tmdbPath: string = "original",
+ filter: string = "original",
width: number = 1920,
quality: number = 75
) {
@@ -276,7 +292,7 @@ export class JellyseerrApi {
? this.axios.defaults.baseURL +
`/_next/image?` +
new URLSearchParams(
- `url=https://image.tmdb.org/t/p/${tmdbPath}/${path}&w=${width}&q=${quality}`
+ `url=https://image.tmdb.org/t/p/${filter}/${path}&w=${width}&q=${quality}`
).toString()
: this.axios?.defaults.baseURL +
`/images/overseerr_poster_not_found_logo_top.png`;
diff --git a/utils/jellyseerr b/utils/jellyseerr
index e69d160e..a15f2ab3 160000
--- a/utils/jellyseerr
+++ b/utils/jellyseerr
@@ -1 +1 @@
-Subproject commit e69d160e25f0962cd77b01c861ce248050e1ad38
+Subproject commit a15f2ab336936f49e38ea37f8b224da40e12588e