mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-11 00:10:24 +01:00
145 lines
4.4 KiB
TypeScript
145 lines
4.4 KiB
TypeScript
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<Record<string, boolean>>({});
|
|
|
|
/**
|
|
* 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<BaseItemDto>) => {
|
|
queryClient.setQueriesData<BaseItemDto | null | undefined>(
|
|
{ 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 /Users/{userId}/Items/{itemId}/Rating?likes=true - add to watchlist
|
|
// DELETE /Users/{userId}/Items/{itemId}/Rating - remove from watchlist
|
|
const path = `/Users/${currentUser.Id}/Items/${currentItem.Id}/Rating`;
|
|
|
|
const response = nextIsWatchlisted
|
|
? await currentApi.post(path, {}, { params: { likes: true } })
|
|
: await currentApi.delete(path, {});
|
|
return response.data;
|
|
},
|
|
onMutate: async (nextIsWatchlisted: boolean) => {
|
|
await queryClient.cancelQueries({ queryKey: itemQueryKeyPrefix });
|
|
|
|
const previousIsWatchlisted = isWatchlisted;
|
|
const previousQueries = queryClient.getQueriesData<BaseItemDto | null>({
|
|
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,
|
|
};
|
|
};
|