forked from Ninjalama/streamyfin_mirror
178 lines
5.0 KiB
TypeScript
178 lines
5.0 KiB
TypeScript
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 { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
|
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { Image } from "expo-image";
|
|
import { useRouter } from "expo-router";
|
|
import { useAtom } from "jotai";
|
|
import React, { useMemo } from "react";
|
|
import { Dimensions, View, ViewProps } from "react-native";
|
|
import { useSharedValue } from "react-native-reanimated";
|
|
import Carousel, {
|
|
ICarouselInstance,
|
|
Pagination,
|
|
} from "react-native-reanimated-carousel";
|
|
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
|
import { Loader } from "../Loader";
|
|
|
|
interface Props extends ViewProps {}
|
|
|
|
export const LargeMovieCarousel: React.FC<Props> = ({ ...props }) => {
|
|
const router = useRouter();
|
|
const queryClient = useQueryClient();
|
|
const [settings] = useSettings();
|
|
|
|
const ref = React.useRef<ICarouselInstance>(null);
|
|
const progress = useSharedValue<number>(0);
|
|
|
|
const [api] = useAtom(apiAtom);
|
|
const [user] = useAtom(userAtom);
|
|
|
|
const { data: sf_carousel, isFetching: l1 } = useQuery({
|
|
queryKey: ["sf_carousel", user?.Id, settings?.mediaListCollectionIds],
|
|
queryFn: async () => {
|
|
if (!api || !user?.Id) return null;
|
|
|
|
const response = await getItemsApi(api).getItems({
|
|
userId: user.Id,
|
|
tags: ["sf_carousel"],
|
|
recursive: true,
|
|
fields: ["Tags"],
|
|
includeItemTypes: ["BoxSet"],
|
|
});
|
|
|
|
return response.data.Items?.[0].Id || null;
|
|
},
|
|
enabled: !!api && !!user?.Id && settings?.usePopularPlugin === true,
|
|
staleTime: 60 * 1000,
|
|
});
|
|
|
|
const onPressPagination = (index: number) => {
|
|
ref.current?.scrollTo({
|
|
/**
|
|
* Calculate the difference between the current index and the target index
|
|
* to ensure that the carousel scrolls to the nearest index
|
|
*/
|
|
count: index - progress.value,
|
|
animated: true,
|
|
});
|
|
};
|
|
|
|
const { data: popularItems, isFetching: l2 } = useQuery<BaseItemDto[]>({
|
|
queryKey: ["popular", user?.Id],
|
|
queryFn: async () => {
|
|
if (!api || !user?.Id || !sf_carousel) return [];
|
|
|
|
const response = await getItemsApi(api).getItems({
|
|
userId: user.Id,
|
|
parentId: sf_carousel,
|
|
limit: 10,
|
|
});
|
|
|
|
return response.data.Items || [];
|
|
},
|
|
enabled: !!api && !!user?.Id && !!sf_carousel,
|
|
staleTime: 60 * 1000,
|
|
});
|
|
|
|
const width = Dimensions.get("screen").width;
|
|
|
|
if (l1 || l2)
|
|
return (
|
|
<View className="h-[242px] flex items-center justify-center">
|
|
<Loader />
|
|
</View>
|
|
);
|
|
|
|
if (!popularItems) return null;
|
|
|
|
return (
|
|
<View className="flex flex-col items-center" {...props}>
|
|
<Carousel
|
|
autoPlay={true}
|
|
autoPlayInterval={3000}
|
|
loop={true}
|
|
ref={ref}
|
|
width={width}
|
|
height={204}
|
|
data={popularItems}
|
|
onProgressChange={progress}
|
|
renderItem={({ item, index }) => <RenderItem item={item} />}
|
|
/>
|
|
<Pagination.Basic
|
|
progress={progress}
|
|
data={popularItems}
|
|
dotStyle={{
|
|
backgroundColor: "rgba(255,255,255,0.2)",
|
|
borderRadius: 50,
|
|
}}
|
|
activeDotStyle={{
|
|
backgroundColor: "rgba(255,255,255,0.8)",
|
|
borderRadius: 50,
|
|
}}
|
|
containerStyle={{ gap: 5, marginTop: 12 }}
|
|
onPress={onPressPagination}
|
|
/>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
|
const [api] = useAtom(apiAtom);
|
|
|
|
const screenWidth = Dimensions.get("screen").width;
|
|
|
|
const uri = useMemo(() => {
|
|
if (!api) return null;
|
|
|
|
return getBackdropUrl({
|
|
api,
|
|
item,
|
|
quality: 70,
|
|
width: Math.floor(screenWidth * 0.8 * 2),
|
|
});
|
|
}, [api, item]);
|
|
|
|
const logoUri = useMemo(() => {
|
|
if (!api) return null;
|
|
return getLogoImageUrlById({ api, item, height: 100 });
|
|
}, [item]);
|
|
|
|
if (!uri || !logoUri) return null;
|
|
|
|
return (
|
|
<TouchableItemRouter item={item}>
|
|
<View className="px-4">
|
|
<View className="relative flex justify-center rounded-2xl overflow-hidden border border-neutral-800">
|
|
<Image
|
|
source={{
|
|
uri,
|
|
}}
|
|
style={{
|
|
width: "100%",
|
|
height: 200,
|
|
borderRadius: 16,
|
|
overflow: "hidden",
|
|
}}
|
|
/>
|
|
<View className="absolute bottom-0 left-0 w-full h-24 p-4 flex items-center">
|
|
<Image
|
|
source={{
|
|
uri: logoUri,
|
|
}}
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
resizeMode: "contain",
|
|
}}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</TouchableItemRouter>
|
|
);
|
|
};
|