diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx index 9a284d33..eb9b660d 100644 --- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/series/[id].tsx @@ -84,7 +84,7 @@ const page: React.FC = () => { allEpisodes && allEpisodes.length > 0 && ( - + {!Platform.isTV && ( <> = ({ item, type, ...props }) => { - const queryClient = useQueryClient(); - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); - - const isFavorite = useMemo(() => { - return item.UserData?.IsFavorite; - }, [item.UserData?.IsFavorite]); - - const updateItemInQueries = (newData: Partial) => { - queryClient.setQueryData( - [type, item.Id], - (old) => { - if (!old) return old; - return { - ...old, - ...newData, - UserData: { ...old.UserData, ...newData.UserData }, - }; - } - ); - }; - - const markFavoriteMutation = useMutation({ - mutationFn: async () => { - if (api && user) { - await getUserLibraryApi(api).markFavoriteItem({ - userId: user.Id, - itemId: item.Id!, - }); - } - }, - onMutate: async () => { - await queryClient.cancelQueries({ queryKey: [type, item.Id] }); - const previousItem = queryClient.getQueryData([ - type, - item.Id, - ]); - updateItemInQueries({ UserData: { IsFavorite: true } }); - - return { previousItem }; - }, - onError: (err, variables, context) => { - if (context?.previousItem) { - queryClient.setQueryData([type, item.Id], context.previousItem); - } - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: [type, item.Id] }); - queryClient.invalidateQueries({ queryKey: ["home", "favorites"] }); - }, - }); - - const unmarkFavoriteMutation = useMutation({ - mutationFn: async () => { - if (api && user) { - await getUserLibraryApi(api).unmarkFavoriteItem({ - userId: user.Id, - itemId: item.Id!, - }); - } - }, - onMutate: async () => { - await queryClient.cancelQueries({ queryKey: [type, item.Id] }); - const previousItem = queryClient.getQueryData([ - type, - item.Id, - ]); - updateItemInQueries({ UserData: { IsFavorite: false } }); - - return { previousItem }; - }, - onError: (err, variables, context) => { - if (context?.previousItem) { - queryClient.setQueryData([type, item.Id], context.previousItem); - } - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: [type, item.Id] }); - queryClient.invalidateQueries({ queryKey: ["home", "favorites"] }); - }, - }); - +export const AddToFavorites = ({ item, ...props }) => { + const { isFavorite, toggleFavorite, _} = useFavorite(item); + return ( { - if (isFavorite) { - unmarkFavoriteMutation.mutate(); - } else { - markFavoriteMutation.mutate(); - } - }} + onPress={toggleFavorite} /> ); diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx index 8fb538dd..1f9077f8 100644 --- a/components/ItemContent.tsx +++ b/components/ItemContent.tsx @@ -98,7 +98,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( )} - + )} diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index d4d53e79..44aceafe 100644 --- a/components/common/TouchableItemRouter.tsx +++ b/components/common/TouchableItemRouter.tsx @@ -1,4 +1,5 @@ import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed"; +import { useFavorite } from "@/hooks/useFavorite"; import { BaseItemDto, BaseItemPerson, @@ -7,7 +8,6 @@ import { useRouter, useSegments } from "expo-router"; import { PropsWithChildren, useCallback } from "react"; import { TouchableOpacity, TouchableOpacityProps } from "react-native"; import { useActionSheet } from "@expo/react-native-action-sheet"; -import { useHaptic } from "@/hooks/useHaptic"; interface Props extends TouchableOpacityProps { item: BaseItemDto; @@ -57,14 +57,14 @@ export const TouchableItemRouter: React.FC> = ({ const segments = useSegments(); const { showActionSheetWithOptions } = useActionSheet(); const markAsPlayedStatus = useMarkAsPlayed([item]); - + const { isFavorite, toggleFavorite, _} = useFavorite(item); + const from = segments[2]; const showActionSheet = useCallback(() => { - if (!(item.Type === "Movie" || item.Type === "Episode")) return; - - const options = ["Mark as Played", "Mark as Not Played", "Cancel"]; - const cancelButtonIndex = 2; + if (!(item.Type === "Movie" || item.Type === "Episode" || item.Type === "Series")) return; + const options = ["Mark as Played", "Mark as Not Played", isFavorite ? "Unmark as Favorite" : "Mark as Favorite", "Cancel"]; + const cancelButtonIndex = 3; showActionSheetWithOptions( { @@ -74,14 +74,14 @@ export const TouchableItemRouter: React.FC> = ({ async (selectedIndex) => { if (selectedIndex === 0) { await markAsPlayedStatus(true); - // Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); } else if (selectedIndex === 1) { await markAsPlayedStatus(false); - // Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + } else if (selectedIndex === 2) { + toggleFavorite() } } ); - }, [showActionSheetWithOptions, markAsPlayedStatus]); + }, [showActionSheetWithOptions, isFavorite, markAsPlayedStatus]); if ( from === "(home)" || diff --git a/hooks/useFavorite.ts b/hooks/useFavorite.ts new file mode 100644 index 00000000..437d290e --- /dev/null +++ b/hooks/useFavorite.ts @@ -0,0 +1,109 @@ +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useAtom } from "jotai"; +import { useEffect, useState, useMemo } from "react"; + +export const useFavorite = (item: BaseItemDto) => { + const queryClient = useQueryClient(); + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + const type = "item"; + const [isFavorite, setIsFavorite] = useState(item.UserData?.IsFavorite); + + useEffect(() => { + setIsFavorite(item.UserData?.IsFavorite); + }, [item.UserData?.IsFavorite]); + + const updateItemInQueries = (newData: Partial) => { + queryClient.setQueryData( + [type, item.Id], + (old) => { + if (!old) return old; + return { + ...old, + ...newData, + UserData: { ...old.UserData, ...newData.UserData }, + }; + } + ); + }; + + const markFavoriteMutation = useMutation({ + mutationFn: async () => { + if (api && user) { + await getUserLibraryApi(api).markFavoriteItem({ + userId: user.Id, + itemId: item.Id!, + }); + } + }, + onMutate: async () => { + await queryClient.cancelQueries({ queryKey: [type, item.Id] }); + const previousItem = queryClient.getQueryData([ + type, + item.Id, + ]); + updateItemInQueries({ UserData: { IsFavorite: true } }); + + return { previousItem }; + }, + onError: (err, variables, context) => { + if (context?.previousItem) { + queryClient.setQueryData([type, item.Id], context.previousItem); + } + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: [type, item.Id] }); + queryClient.invalidateQueries({ queryKey: ["home", "favorites"] }); + setIsFavorite(true); + }, + }); + + const unmarkFavoriteMutation = useMutation({ + mutationFn: async () => { + if (api && user) { + await getUserLibraryApi(api).unmarkFavoriteItem({ + userId: user.Id, + itemId: item.Id!, + }); + } + }, + onMutate: async () => { + await queryClient.cancelQueries({ queryKey: [type, item.Id] }); + const previousItem = queryClient.getQueryData([ + type, + item.Id, + ]); + updateItemInQueries({ UserData: { IsFavorite: false } }); + + return { previousItem }; + }, + onError: (err, variables, context) => { + if (context?.previousItem) { + queryClient.setQueryData([type, item.Id], context.previousItem); + } + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: [type, item.Id] }); + queryClient.invalidateQueries({ queryKey: ["home", "favorites"] }); + setIsFavorite(false); + }, + }); + + const toggleFavorite = () => { + if (isFavorite) { + unmarkFavoriteMutation.mutate(); + } else { + markFavoriteMutation.mutate(); + } + }; + + return { + isFavorite, + toggleFavorite, + markFavoriteMutation, + unmarkFavoriteMutation, + }; +};