diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx
index 1ae0059c..2a7d6a57 100644
--- a/app/(auth)/(tabs)/(search)/index.tsx
+++ b/app/(auth)/(tabs)/(search)/index.tsx
@@ -43,6 +43,9 @@ import DiscoverSlide from "@/components/jellyseerr/DiscoverSlide";
import { sortBy } from "lodash";
import PersonPoster from "@/components/jellyseerr/PersonPoster";
import { useReactNavigationQuery } from "@/utils/useReactNavigationQuery";
+import { SearchItemWrapper } from "@/components/search/SearchItemWrapper";
+import { JellyserrIndexPage } from "@/components/jellyseerr/JellyseerrIndexPage";
+import { LoadingSkeleton } from "@/components/search/LoadingSkeleton";
type SearchType = "Library" | "Discover";
@@ -155,57 +158,6 @@ export default function search() {
enabled: searchType === "Library" && debouncedSearch.length > 0,
});
- const { data: jellyseerrResults, isFetching: j1 } = useReactNavigationQuery({
- queryKey: ["search", "jellyseerr", "results", debouncedSearch],
- queryFn: async () => {
- const response = await jellyseerrApi?.search({
- query: new URLSearchParams(debouncedSearch).toString(),
- page: 1, // todo: maybe rework page & page-size if first results are not enough...
- language: "en",
- });
-
- return response?.results;
- },
- enabled:
- !!jellyseerrApi &&
- searchType === "Discover" &&
- debouncedSearch.length > 0,
- });
-
- const { data: jellyseerrDiscoverSettings, isFetching: j2 } =
- useReactNavigationQuery({
- queryKey: ["search", "jellyseerr", "discoverSettings", debouncedSearch],
- queryFn: async () => jellyseerrApi?.discoverSettings(),
- enabled:
- !!jellyseerrApi &&
- searchType === "Discover" &&
- debouncedSearch.length == 0,
- });
-
- const jellyseerrMovieResults: MovieResult[] | undefined = useMemo(
- () =>
- jellyseerrResults?.filter(
- (r) => r.mediaType === MediaType.MOVIE
- ) as MovieResult[],
- [jellyseerrResults]
- );
-
- const jellyseerrTvResults: TvResult[] | undefined = useMemo(
- () =>
- jellyseerrResults?.filter(
- (r) => r.mediaType === MediaType.TV
- ) as TvResult[],
- [jellyseerrResults]
- );
-
- const jellyseerrPersonResults: PersonResult[] | undefined = useMemo(
- () =>
- jellyseerrResults?.filter(
- (r) => r.mediaType === "person"
- ) as PersonResult[],
- [jellyseerrResults]
- );
-
const { data: series, isFetching: l2 } = useQuery({
queryKey: ["search", "series", debouncedSearch],
queryFn: () =>
@@ -285,25 +237,13 @@ export default function search() {
episodes?.length ||
series?.length ||
collections?.length ||
- actors?.length ||
- jellyseerrMovieResults?.length ||
- jellyseerrTvResults?.length
+ actors?.length
);
- }, [
- artists,
- episodes,
- albums,
- songs,
- movies,
- series,
- collections,
- actors,
- jellyseerrResults,
- ]);
+ }, [artists, episodes, albums, songs, movies, series, collections, actors]);
const loading = useMemo(() => {
- return l1 || l2 || l3 || l4 || l5 || l6 || l7 || l8 || j1 || j2;
- }, [l1, l2, l3, l4, l5, l6, l7, l8, j1, j2]);
+ return l1 || l2 || l3 || l4 || l5 || l6 || l7 || l8;
+ }, [l1, l2, l3, l4, l5, l6, l7, l8]);
return (
<>
@@ -350,15 +290,13 @@ export default function search() {
)}
- {!!q && (
-
-
- Results for {q}
-
-
- )}
- {searchType === "Library" && (
- <>
+
+
+
+
+
+ {searchType === "Library" ? (
+
m.Id!)}
@@ -483,139 +421,41 @@ export default function search() {
)}
/>
- >
- )}
- {searchType === "Discover" && (
+
+ ) : (
<>
- (
-
- )}
- />
- (
-
- )}
- />
- (
-
- )}
- />
+
>
)}
- {loading ? (
-
-
-
- ) : noResults && debouncedSearch.length > 0 ? (
-
-
- No results found for
-
-
- "{debouncedSearch}"
-
-
- ) : debouncedSearch.length === 0 && searchType === "Library" ? (
-
- {exampleSearches.map((e) => (
- setSearch(e)}
- key={e}
- className="mb-2"
- >
- {e}
-
- ))}
-
- ) : debouncedSearch.length === 0 && searchType === "Discover" ? (
-
- {sortBy?.(
- jellyseerrDiscoverSettings?.filter((s) => s.enabled),
- "order"
- ).map((slide) => (
-
- ))}
-
- ) : null}
+ {searchType === "Library" && (
+ <>
+ {!loading && noResults && debouncedSearch.length > 0 ? (
+
+
+ No results found for
+
+
+ "{debouncedSearch}"
+
+
+ ) : debouncedSearch.length === 0 ? (
+
+ {exampleSearches.map((e) => (
+ setSearch(e)}
+ key={e}
+ className="mb-2"
+ >
+ {e}
+
+ ))}
+
+ ) : null}
+ >
+ )}
>
);
}
-
-type Props = {
- ids?: string[] | null;
- items?: T[];
- renderItem: (item: any) => React.ReactNode;
- header?: string;
-};
-
-const SearchItemWrapper = ({
- ids,
- items,
- renderItem,
- header,
-}: PropsWithChildren>) => {
- const [api] = useAtom(apiAtom);
- const [user] = useAtom(userAtom);
-
- const { data, isLoading: l1 } = useQuery({
- queryKey: ["items", ids],
- queryFn: async () => {
- if (!user?.Id || !api || !ids || ids.length === 0) {
- return [];
- }
-
- const itemPromises = ids.map((id) =>
- getUserItemData({
- api,
- userId: user.Id,
- itemId: id,
- })
- );
-
- const results = await Promise.all(itemPromises);
-
- // Filter out null items
- return results.filter(
- (item) => item !== null
- ) as unknown as BaseItemDto[];
- },
- enabled: !!ids && ids.length > 0 && !!api && !!user?.Id,
- staleTime: Infinity,
- });
-
- if (!data && (!items || items.length === 0)) return null;
-
- return (
- <>
- {header}
-
- {data && data?.length > 0
- ? data.map((item) => renderItem(item))
- : items && items?.length > 0
- ? items.map((i) => renderItem(i))
- : undefined}
-
- >
- );
-};
diff --git a/components/jellyseerr/JellyseerrIndexPage.tsx b/components/jellyseerr/JellyseerrIndexPage.tsx
new file mode 100644
index 00000000..f85b9807
--- /dev/null
+++ b/components/jellyseerr/JellyseerrIndexPage.tsx
@@ -0,0 +1,172 @@
+import { useJellyseerr } from "@/hooks/useJellyseerr";
+import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
+import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
+import { MediaType } from "@/utils/jellyseerr/server/constants/media";
+import {
+ MovieResult,
+ PersonResult,
+ TvResult,
+} from "@/utils/jellyseerr/server/models/Search";
+import { useReactNavigationQuery } from "@/utils/useReactNavigationQuery";
+import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
+import { useQuery } from "@tanstack/react-query";
+import { useAtom } from "jotai";
+import { PropsWithChildren, useMemo } from "react";
+import { ScrollView, View, ViewProps } from "react-native";
+import { Text } from "../common/Text";
+import JellyseerrPoster from "../posters/JellyseerrPoster";
+import PersonPoster from "./PersonPoster";
+import { SearchItemWrapper } from "../search/SearchItemWrapper";
+import DiscoverSlide from "./DiscoverSlide";
+import { sortBy } from "lodash";
+import { Loader } from "../Loader";
+import Animated, {
+ withTiming,
+ useAnimatedStyle,
+ withSequence,
+ useSharedValue,
+ useAnimatedReaction,
+} from "react-native-reanimated";
+import { LoadingSkeleton } from "../search/LoadingSkeleton";
+
+interface Props extends ViewProps {
+ searchQuery: string;
+}
+
+export const JellyserrIndexPage: React.FC = ({ searchQuery }) => {
+ const { jellyseerrApi } = useJellyseerr();
+ const opacity = useSharedValue(1);
+
+ const {
+ data: jellyseerrDiscoverSettings,
+ isFetching: f1,
+ isLoading: l1,
+ } = useReactNavigationQuery({
+ queryKey: ["search", "jellyseerr", "discoverSettings", searchQuery],
+ queryFn: async () => jellyseerrApi?.discoverSettings(),
+ enabled: !!jellyseerrApi && searchQuery.length == 0,
+ });
+
+ const {
+ data: jellyseerrResults,
+ isFetching: f2,
+ isLoading: l2,
+ } = useReactNavigationQuery({
+ queryKey: ["search", "jellyseerr", "results", searchQuery],
+ queryFn: async () => {
+ const response = await jellyseerrApi?.search({
+ query: new URLSearchParams(searchQuery).toString(),
+ page: 1,
+ language: "en",
+ });
+ return response?.results;
+ },
+ enabled: !!jellyseerrApi && searchQuery.length > 0,
+ });
+
+ const animatedStyle = useAnimatedStyle(() => {
+ return {
+ opacity: opacity.value,
+ };
+ });
+
+ useAnimatedReaction(
+ () => f1 || f2 || l1 || l2,
+ (isLoading) => {
+ if (isLoading) {
+ opacity.value = withTiming(1, { duration: 200 });
+ } else {
+ opacity.value = withTiming(0, { duration: 200 });
+ }
+ }
+ );
+
+ const jellyseerrMovieResults = useMemo(
+ () =>
+ jellyseerrResults?.filter(
+ (r) => r.mediaType === MediaType.MOVIE
+ ) as MovieResult[],
+ [jellyseerrResults]
+ );
+
+ const jellyseerrTvResults = useMemo(
+ () =>
+ jellyseerrResults?.filter(
+ (r) => r.mediaType === MediaType.TV
+ ) as TvResult[],
+ [jellyseerrResults]
+ );
+
+ const jellyseerrPersonResults = useMemo(
+ () =>
+ jellyseerrResults?.filter(
+ (r) => r.mediaType === "person"
+ ) as PersonResult[],
+ [jellyseerrResults]
+ );
+
+ if (!searchQuery.length)
+ return (
+
+ {sortBy?.(
+ jellyseerrDiscoverSettings?.filter((s) => s.enabled),
+ "order"
+ ).map((slide) => (
+
+ ))}
+
+ );
+
+ return (
+
+
+
+ {!jellyseerrMovieResults?.length &&
+ !jellyseerrTvResults?.length &&
+ !jellyseerrPersonResults?.length &&
+ !f1 &&
+ !f2 &&
+ !l1 &&
+ !l2 && (
+
+
+ No results found for
+
+
+ "{searchQuery}"
+
+
+ )}
+
+
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+
+
+ );
+};
diff --git a/components/search/LoadingSkeleton.tsx b/components/search/LoadingSkeleton.tsx
new file mode 100644
index 00000000..8ac38ada
--- /dev/null
+++ b/components/search/LoadingSkeleton.tsx
@@ -0,0 +1,66 @@
+import { View } from "react-native";
+import { Text } from "../common/Text";
+import Animated, {
+ useAnimatedStyle,
+ useAnimatedReaction,
+ useSharedValue,
+ withTiming,
+} from "react-native-reanimated";
+
+interface Props {
+ isLoading: boolean;
+}
+
+export const LoadingSkeleton: React.FC = ({ isLoading }) => {
+ const opacity = useSharedValue(1);
+
+ const animatedStyle = useAnimatedStyle(() => {
+ return {
+ opacity: opacity.value,
+ };
+ });
+
+ useAnimatedReaction(
+ () => isLoading,
+ (loading) => {
+ if (loading) {
+ opacity.value = withTiming(1, { duration: 200 });
+ } else {
+ opacity.value = withTiming(0, { duration: 200 });
+ }
+ }
+ );
+
+ return (
+
+ {[1, 2, 3].map((s) => (
+
+
+
+ {[1, 2, 3].map((i) => (
+
+
+
+
+ Nisi mollit voluptate amet.
+
+
+
+
+ Lorem ipsum
+
+
+
+ ))}
+
+
+ ))}
+
+ );
+};
diff --git a/components/search/SearchItemWrapper.tsx b/components/search/SearchItemWrapper.tsx
new file mode 100644
index 00000000..45c3e341
--- /dev/null
+++ b/components/search/SearchItemWrapper.tsx
@@ -0,0 +1,70 @@
+import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
+import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
+import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
+import { useQuery } from "@tanstack/react-query";
+import { useAtom } from "jotai";
+import { PropsWithChildren } from "react";
+import { ScrollView } from "react-native";
+import { Text } from "../common/Text";
+
+type SearchItemWrapperProps = {
+ ids?: string[] | null;
+ items?: T[];
+ renderItem: (item: any) => React.ReactNode;
+ header?: string;
+};
+
+export const SearchItemWrapper = ({
+ ids,
+ items,
+ renderItem,
+ header,
+}: PropsWithChildren>) => {
+ const [api] = useAtom(apiAtom);
+ const [user] = useAtom(userAtom);
+
+ const { data, isLoading: l1 } = useQuery({
+ queryKey: ["items", ids],
+ queryFn: async () => {
+ if (!user?.Id || !api || !ids || ids.length === 0) {
+ return [];
+ }
+
+ const itemPromises = ids.map((id) =>
+ getUserItemData({
+ api,
+ userId: user.Id,
+ itemId: id,
+ })
+ );
+
+ const results = await Promise.all(itemPromises);
+
+ // Filter out null items
+ return results.filter(
+ (item) => item !== null
+ ) as unknown as BaseItemDto[];
+ },
+ enabled: !!ids && ids.length > 0 && !!api && !!user?.Id,
+ staleTime: Infinity,
+ });
+
+ if (!data && (!items || items.length === 0)) return null;
+
+ return (
+ <>
+ {header}
+
+ {data && data?.length > 0
+ ? data.map((item) => renderItem(item))
+ : items && items?.length > 0
+ ? items.map((i) => renderItem(i))
+ : undefined}
+
+ >
+ );
+};