import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { atom, useAtom } from "jotai"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; // Shared atom to store watchlist (Likes) status across all components // Maps itemId -> isWatchlisted const watchlistAtom = atom>({}); /** * KefinTweaks watchlist is backed by Jellyfin's native "Likes" rating. * Toggling watchlist membership toggles UserData.Likes on the item. */ export const useWatchlist = (item: BaseItemDto) => { const queryClient = useQueryClient(); const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const [watchlist, setWatchlist] = useAtom(watchlistAtom); const itemId = item.Id ?? ""; // Get current watchlist status from shared state, falling back to item data const isWatchlisted = itemId ? (watchlist[itemId] ?? item.UserData?.Likes) : item.UserData?.Likes; // Update shared state when item data changes useEffect(() => { if (itemId && item.UserData?.Likes !== undefined) { setWatchlist((prev) => ({ ...prev, [itemId]: item.UserData!.Likes!, })); } }, [itemId, item.UserData?.Likes, setWatchlist]); // Helper to update watchlist status in shared state const setIsWatchlisted = useCallback( (value: boolean | null | undefined) => { if (itemId && typeof value === "boolean") { setWatchlist((prev) => ({ ...prev, [itemId]: value })); } }, [itemId, setWatchlist], ); // Use refs to avoid stale closure issues in mutationFn const itemRef = useRef(item); const apiRef = useRef(api); const userRef = useRef(user); // Keep refs updated useEffect(() => { itemRef.current = item; }, [item]); useEffect(() => { apiRef.current = api; }, [api]); useEffect(() => { userRef.current = user; }, [user]); const itemQueryKeyPrefix = useMemo( () => ["item", item.Id] as const, [item.Id], ); const updateItemInQueries = useCallback( (newData: Partial) => { queryClient.setQueriesData( { queryKey: itemQueryKeyPrefix }, (old) => { if (!old) return old; return { ...old, ...newData, UserData: { ...old.UserData, ...newData.UserData }, }; }, ); }, [itemQueryKeyPrefix, queryClient], ); const watchlistMutation = useMutation({ mutationFn: async (nextIsWatchlisted: boolean) => { const currentApi = apiRef.current; const currentUser = userRef.current; const currentItem = itemRef.current; if (!currentApi || !currentUser?.Id || !currentItem?.Id) { return; } // Watchlist == Jellyfin "Likes" rating: // POST /UserItems/{itemId}/Rating?userId={userId}&likes=true - add to watchlist // POST /UserItems/{itemId}/Rating?userId={userId}&likes=false - remove from watchlist const path = `/UserItems/${currentItem.Id}/Rating`; const response = await currentApi.post( path, {}, { params: { userId: currentUser.Id, likes: nextIsWatchlisted } }, ); return response.data; }, onMutate: async (nextIsWatchlisted: boolean) => { await queryClient.cancelQueries({ queryKey: itemQueryKeyPrefix }); const previousIsWatchlisted = isWatchlisted; const previousQueries = queryClient.getQueriesData({ queryKey: itemQueryKeyPrefix, }); setIsWatchlisted(nextIsWatchlisted); updateItemInQueries({ UserData: { Likes: nextIsWatchlisted } }); return { previousIsWatchlisted, previousQueries }; }, onError: (_err, _nextIsWatchlisted, context) => { if (context?.previousQueries) { for (const [queryKey, data] of context.previousQueries) { queryClient.setQueryData(queryKey, data); } } setIsWatchlisted(context?.previousIsWatchlisted); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: itemQueryKeyPrefix }); queryClient.invalidateQueries({ queryKey: ["home", "watchlist"] }); }, }); const toggleWatchlist = useCallback(() => { watchlistMutation.mutate(!isWatchlisted); }, [watchlistMutation, isWatchlisted]); return { isWatchlisted, toggleWatchlist, watchlistMutation, }; };