feat: context menu actions for items

This commit is contained in:
Fredrik Burmester
2024-12-22 11:57:44 +01:00
parent 030947fc38
commit 8608ad02f7
3 changed files with 168 additions and 74 deletions

View File

@@ -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<Props> = ({ item, ...props }) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const queryClient = useQueryClient();
const invalidateQueries = () => {
@@ -46,68 +39,14 @@ export const PlayedStatus: React.FC<Props> = ({ 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 (
<View {...props}>
<RoundButton
fillColor={item.UserData?.Played ? "primary" : undefined}
icon={item.UserData?.Played ? "checkmark" : "checkmark"}
onPress={() => handlePress(item.UserData?.Played || false)}
onPress={() => markAsPlayedStatus(item.UserData?.Played || false)}
size="large"
/>
</View>

View File

@@ -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<PropsWithChildren<Props>> = ({
const from = segments[2];
const markAsPlayedStatus = useMarkAsPlayed(item);
if (from === "(home)" || from === "(search)" || from === "(libraries)")
return (
<TouchableOpacity
onPress={() => {
const url = itemRouter(item, from);
// @ts-ignore
router.push(url);
}}
{...props}
>
{children}
</TouchableOpacity>
<ContextMenu.Root>
<ContextMenu.Trigger>
<TouchableOpacity
onPress={() => {
const url = itemRouter(item, from);
// @ts-ignore
router.push(url);
}}
{...props}
>
{children}
</TouchableOpacity>
</ContextMenu.Trigger>
<ContextMenu.Content
avoidCollisions
alignOffset={0}
collisionPadding={0}
loop={false}
key={"content"}
>
<ContextMenu.Label key="label-1">Actions</ContextMenu.Label>
<ContextMenu.Item
key="item-1"
onSelect={() => {
markAsPlayedStatus(true);
}}
shouldDismissMenuOnSelect
>
<ContextMenu.ItemTitle key="item-1-title">
Mark as watched
</ContextMenu.ItemTitle>
<ContextMenu.ItemIcon
ios={{
name: "checkmark.circle", // Changed to "checkmark.circle" which represents "watched"
pointSize: 18,
weight: "semibold",
scale: "medium",
hierarchicalColor: {
dark: "green", // Changed to green for "watched"
light: "green",
},
}}
androidIconName="checkmark-circle"
></ContextMenu.ItemIcon>
</ContextMenu.Item>
<ContextMenu.Item
key="item-2"
onSelect={() => {
markAsPlayedStatus(true);
}}
shouldDismissMenuOnSelect
destructive
>
<ContextMenu.ItemTitle key="item-2-title">
Mark as not watched
</ContextMenu.ItemTitle>
<ContextMenu.ItemIcon
ios={{
name: "eye.slash", // Changed to "eye.slash" which represents "not watched"
pointSize: 18, // Adjusted for better visibility
weight: "semibold",
scale: "medium",
hierarchicalColor: {
dark: "red", // Changed to red for "not watched"
light: "red",
},
// Removed paletteColors as it's not necessary in this case
}}
androidIconName="eye-slash"
></ContextMenu.ItemIcon>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
);
};

88
hooks/useMarkAsPlayed.ts Normal file
View File

@@ -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;
};