diff --git a/components/PlayedStatus.tsx b/components/PlayedStatus.tsx index d169d347..15890a9f 100644 --- a/components/PlayedStatus.tsx +++ b/components/PlayedStatus.tsx @@ -1,10 +1,6 @@ -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { markAsNotPlayed } from "@/utils/jellyfin/playstate/markAsNotPlayed"; -import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed"; +import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useQueryClient } from "@tanstack/react-query"; -import * as Haptics from "expo-haptics"; -import { useAtom } from "jotai"; import React from "react"; import { View, ViewProps } from "react-native"; import { RoundButton } from "./RoundButton"; @@ -14,9 +10,6 @@ interface Props extends ViewProps { } export const PlayedStatus: React.FC = ({ item, ...props }) => { - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); - const queryClient = useQueryClient(); const invalidateQueries = () => { @@ -46,68 +39,14 @@ export const PlayedStatus: React.FC = ({ item, ...props }) => { }); }; - const handlePress = async (played: boolean) => { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - - // Optimistic update - queryClient.setQueryData( - ["item", item.Id], - (oldData: BaseItemDto | undefined) => { - if (oldData) { - return { - ...oldData, - UserData: { - ...oldData.UserData, - Played: !played, - }, - }; - } - return oldData; - } - ); - - try { - if (played) { - await markAsNotPlayed({ - api: api, - itemId: item?.Id, - userId: user?.Id, - }); - } else { - await markAsPlayed({ - api: api, - item: item, - userId: user?.Id, - }); - } - invalidateQueries(); - } catch (error) { - // Revert optimistic update on error - queryClient.setQueryData( - ["item", item.Id], - (oldData: BaseItemDto | undefined) => { - if (oldData) { - return { - ...oldData, - UserData: { - ...oldData.UserData, - Played: played, - }, - }; - } - return oldData; - } - ); - console.error("Error updating played status:", error); - } - }; + const markAsPlayedStatus = useMarkAsPlayed(item); return ( handlePress(item.UserData?.Played || false)} + onPress={() => markAsPlayedStatus(item.UserData?.Played || false)} size="large" /> diff --git a/components/common/TouchableItemRouter.tsx b/components/common/TouchableItemRouter.tsx index 012739e1..cd5fce45 100644 --- a/components/common/TouchableItemRouter.tsx +++ b/components/common/TouchableItemRouter.tsx @@ -1,8 +1,10 @@ +import { useMarkAsPlayed } from "@/hooks/useMarkAsPlayed"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import * as Haptics from "expo-haptics"; import { useRouter, useSegments } from "expo-router"; import { PropsWithChildren } from "react"; import { TouchableOpacity, TouchableOpacityProps } from "react-native"; +import * as ContextMenu from "zeego/context-menu"; interface Props extends TouchableOpacityProps { item: BaseItemDto; @@ -62,17 +64,82 @@ export const TouchableItemRouter: React.FC> = ({ const from = segments[2]; + const markAsPlayedStatus = useMarkAsPlayed(item); + if (from === "(home)" || from === "(search)" || from === "(libraries)") return ( - { - const url = itemRouter(item, from); - // @ts-ignore - router.push(url); - }} - {...props} - > - {children} - + + + { + const url = itemRouter(item, from); + // @ts-ignore + router.push(url); + }} + {...props} + > + {children} + + + + Actions + { + markAsPlayedStatus(true); + }} + shouldDismissMenuOnSelect + > + + Mark as watched + + + + { + markAsPlayedStatus(true); + }} + shouldDismissMenuOnSelect + destructive + > + + Mark as not watched + + + + + ); }; diff --git a/hooks/useMarkAsPlayed.ts b/hooks/useMarkAsPlayed.ts new file mode 100644 index 00000000..ff039cc8 --- /dev/null +++ b/hooks/useMarkAsPlayed.ts @@ -0,0 +1,88 @@ +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { markAsNotPlayed } from "@/utils/jellyfin/playstate/markAsNotPlayed"; +import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { useQueryClient } from "@tanstack/react-query"; +import * as Haptics from "expo-haptics"; +import { useAtom } from "jotai"; + +export const useMarkAsPlayed = (item: BaseItemDto) => { + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + const queryClient = useQueryClient(); + + const invalidateQueries = () => { + const queriesToInvalidate = [ + ["item", item.Id], + ["resumeItems"], + ["continueWatching"], + ["nextUp-all"], + ["nextUp"], + ["episodes"], + ["seasons"], + ["home"], + ]; + + queriesToInvalidate.forEach((queryKey) => { + queryClient.invalidateQueries({ queryKey }); + }); + }; + + const markAsPlayedStatus = async (played: boolean) => { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + + // Optimistic update + queryClient.setQueryData( + ["item", item.Id], + (oldData: BaseItemDto | undefined) => { + if (oldData) { + return { + ...oldData, + UserData: { + ...oldData.UserData, + Played: !played, + }, + }; + } + return oldData; + } + ); + + try { + if (played) { + await markAsNotPlayed({ + api: api, + itemId: item?.Id, + userId: user?.Id, + }); + } else { + await markAsPlayed({ + api: api, + item: item, + userId: user?.Id, + }); + } + invalidateQueries(); + } catch (error) { + // Revert optimistic update on error + queryClient.setQueryData( + ["item", item.Id], + (oldData: BaseItemDto | undefined) => { + if (oldData) { + return { + ...oldData, + UserData: { + ...oldData.UserData, + Played: played, + }, + }; + } + return oldData; + } + ); + console.error("Error updating played status:", error); + } + }; + + return markAsPlayedStatus; +};