From 4a17a00f81a54de9722ca888afc201966b16a9c9 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sat, 17 Aug 2024 13:15:25 +0200 Subject: [PATCH] feat: more media list stuff --- components/home/LargeMovieCarousel.tsx | 171 +++++++++++++++++++++ components/medialists/MediaListSection.tsx | 56 +++++++ components/settings/SettingToggles.tsx | 126 ++++++++++++--- 3 files changed, 335 insertions(+), 18 deletions(-) create mode 100644 components/home/LargeMovieCarousel.tsx create mode 100644 components/medialists/MediaListSection.tsx diff --git a/components/home/LargeMovieCarousel.tsx b/components/home/LargeMovieCarousel.tsx new file mode 100644 index 00000000..b66105e2 --- /dev/null +++ b/components/home/LargeMovieCarousel.tsx @@ -0,0 +1,171 @@ +import { View, ViewProps } from "react-native"; +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 { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { useRouter } from "expo-router"; +import { useAtom } from "jotai"; +import { useSettings } from "@/utils/atoms/settings"; +import { Dimensions } from "react-native"; +import { useSharedValue } from "react-native-reanimated"; +import Carousel, { + ICarouselInstance, + Pagination, +} from "react-native-reanimated-carousel"; +import React, { useMemo } from "react"; +import { Image } from "expo-image"; +import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; +import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; +import { TouchableItemRouter } from "../common/TouchableItemRouter"; + +interface Props extends ViewProps {} + +export const LargeMovieCarousel: React.FC = ({ ...props }) => { + const router = useRouter(); + const queryClient = useQueryClient(); + const [settings] = useSettings(); + + const ref = React.useRef(null); + const progress = useSharedValue(0); + + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + + 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: mediaListCollection } = useQuery({ + queryKey: ["mediaListCollection", user?.Id], + queryFn: async () => { + if (!api || !user?.Id) return null; + + const response = await getItemsApi(api).getItems({ + userId: user.Id, + tags: ["medialist", "promoted"], + recursive: true, + fields: ["Tags"], + includeItemTypes: ["BoxSet"], + }); + + const id = response.data.Items?.find((c) => c.Name === "sf_carousel")?.Id; + return id || null; + }, + enabled: !!api && !!user?.Id && settings?.usePopularPlugin === true, + staleTime: 0, + }); + + const { data: popularItems, isLoading: isLoadingPopular } = useQuery< + BaseItemDto[] + >({ + queryKey: ["popular", user?.Id], + queryFn: async () => { + if (!api || !user?.Id || !mediaListCollection) return []; + + const response = await getItemsApi(api).getItems({ + userId: user.Id, + parentId: mediaListCollection, + limit: 10, + }); + + return response.data.Items || []; + }, + enabled: !!api && !!user?.Id && !!mediaListCollection, + staleTime: 0, + }); + + const width = Dimensions.get("screen").width; + + if (!popularItems) return null; + + return ( + + } + /> + + + ); +}; + +const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => { + const [api] = useAtom(apiAtom); + + const uri = useMemo(() => { + if (!api) return null; + + return getBackdropUrl({ + api, + item, + quality: 90, + width: 1000, + }); + }, [api, item]); + + const logoUri = useMemo(() => { + if (!api) return null; + return getLogoImageUrlById({ api, item, height: 100 }); + }, [item]); + + if (!uri || !logoUri) return null; + + return ( + + + + + + + + + + + ); +}; diff --git a/components/medialists/MediaListSection.tsx b/components/medialists/MediaListSection.tsx new file mode 100644 index 00000000..35ce7b94 --- /dev/null +++ b/components/medialists/MediaListSection.tsx @@ -0,0 +1,56 @@ +import { View, ViewProps } from "react-native"; +import { Text } from "@/components/common/Text"; +import settings from "@/app/(auth)/settings"; +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 { ScrollingCollectionList } from "../home/ScrollingCollectionList"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { useSettings } from "@/utils/atoms/settings"; +import { useRouter } from "expo-router"; +import { useAtom } from "jotai"; +import { useState } from "react"; + +interface Props extends ViewProps { + collection: BaseItemDto; +} + +export const MediaListSection: React.FC = ({ collection, ...props }) => { + const router = useRouter(); + const queryClient = useQueryClient(); + + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + + const [loading, setLoading] = useState(false); + const [settings, _] = useSettings(); + + const { data: popularItems, isLoading: isLoadingPopular } = useQuery< + BaseItemDto[] + >({ + queryKey: ["popular", user?.Id], + queryFn: async () => { + if (!api || !user?.Id || !collection.Id) return []; + + const response = await getItemsApi(api).getItems({ + userId: user.Id, + parentId: collection.Id, + limit: 10, + }); + + return response.data.Items || []; + }, + enabled: !!api && !!user?.Id && !!collection.Id, + staleTime: 0, + }); + + return ( + + ); +}; diff --git a/components/settings/SettingToggles.tsx b/components/settings/SettingToggles.tsx index 042fd524..f64d6db4 100644 --- a/components/settings/SettingToggles.tsx +++ b/components/settings/SettingToggles.tsx @@ -1,11 +1,50 @@ -import { Linking, Switch, TouchableOpacity, View } from "react-native"; +import { + ActivityIndicator, + Linking, + Switch, + TouchableOpacity, + View, +} from "react-native"; import { Text } from "../common/Text"; import { useSettings } from "@/utils/atoms/settings"; import * as DropdownMenu from "zeego/dropdown-menu"; +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; +import { useQuery } from "@tanstack/react-query"; +import { useAtom } from "jotai"; +import { useState } from "react"; export const SettingToggles: React.FC = () => { const [settings, updateSettings] = useSettings(); + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + + const { + data: mediaListCollections, + isLoading: isLoadingMediaListCollections, + } = useQuery({ + queryKey: ["mediaListCollections", user?.Id], + queryFn: async () => { + if (!api || !user?.Id) return []; + + const response = await getItemsApi(api).getItems({ + userId: user.Id, + tags: ["medialist", "promoted"], + recursive: true, + fields: ["Tags"], + includeItemTypes: ["BoxSet"], + }); + + const ids = + response.data.Items?.filter((c) => c.Name !== "sf_carousel") ?? []; + + return ids; + }, + enabled: !!api && !!user?.Id && settings?.usePopularPlugin === true, + staleTime: 0, + }); + return ( @@ -36,25 +75,76 @@ export const SettingToggles: React.FC = () => { } /> - - - Use popular lists plugin - Made by: lostb1t - { - Linking.openURL( - "https://github.com/lostb1t/jellyfin-plugin-media-lists", - ); - }} - > - More info - + + + + Use popular lists plugin + Made by: lostb1t + { + Linking.openURL( + "https://github.com/lostb1t/jellyfin-plugin-media-lists", + ); + }} + > + More info + + + + updateSettings({ usePopularPlugin: value }) + } + /> - updateSettings({ usePopularPlugin: value })} - /> + {settings?.usePopularPlugin && ( + + {mediaListCollections?.map((mlc) => ( + + + {mlc.Name} + + { + if (!settings.mediaListCollectionIds) { + updateSettings({ + mediaListCollectionIds: [mlc.Id!], + }); + return; + } + + updateSettings({ + mediaListCollectionIds: + settings?.mediaListCollectionIds.includes(mlc.Id!) + ? settings?.mediaListCollectionIds.filter( + (id) => id !== mlc.Id, + ) + : [...settings?.mediaListCollectionIds, mlc.Id!], + }); + }} + /> + + ))} + {isLoadingMediaListCollections && ( + + + + )} + {mediaListCollections?.length === 0 && ( + + + No collections found. Add some in Jellyfin. + + + )} + + )} + Force direct play