feat: Mark entire seasons of a show as played (#445)

This commit is contained in:
Max Ward
2025-01-29 01:54:00 -08:00
committed by GitHub
parent c4d4475aa9
commit 730ef4616f
5 changed files with 81 additions and 59 deletions

View File

@@ -90,7 +90,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
{item.Type !== "Program" && ( {item.Type !== "Program" && (
<View className="flex flex-row items-center space-x-2"> <View className="flex flex-row items-center space-x-2">
<DownloadSingleItem item={item} size="large" /> <DownloadSingleItem item={item} size="large" />
<PlayedStatus item={item} /> <PlayedStatus items={[item]} size="large" />
<AddToFavorites item={item} type="item" /> <AddToFavorites item={item} type="item" />
</View> </View>
)} )}

View File

@@ -6,16 +6,19 @@ import { View, ViewProps } from "react-native";
import { RoundButton } from "./RoundButton"; import { RoundButton } from "./RoundButton";
interface Props extends ViewProps { interface Props extends ViewProps {
item: BaseItemDto; items: BaseItemDto[];
size?: "default" | "large";
} }
export const PlayedStatus: React.FC<Props> = ({ item, ...props }) => { export const PlayedStatus: React.FC<Props> = ({ items, ...props }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const invalidateQueries = () => { const invalidateQueries = () => {
queryClient.invalidateQueries({ items.forEach((item) => {
queryKey: ["item", item.Id], queryClient.invalidateQueries({
}); queryKey: ["item", item.Id],
});
})
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["resumeItems"], queryKey: ["resumeItems"],
}); });
@@ -39,15 +42,20 @@ export const PlayedStatus: React.FC<Props> = ({ item, ...props }) => {
}); });
}; };
const markAsPlayedStatus = useMarkAsPlayed(item); const allPlayed = items.every((item) => item.UserData?.Played);
const markAsPlayedStatus = useMarkAsPlayed(items);
return ( return (
<View {...props}> <View {...props}>
<RoundButton <RoundButton
fillColor={item.UserData?.Played ? "primary" : undefined} fillColor={allPlayed ? "primary" : undefined}
icon={item.UserData?.Played ? "checkmark" : "checkmark"} icon={allPlayed ? "checkmark" : "checkmark"}
onPress={() => markAsPlayedStatus(!item.UserData?.Played)} onPress={async () => {
size="large" console.log(allPlayed);
await markAsPlayedStatus(!allPlayed)
}}
size={props.size}
/> />
</View> </View>
); );

View File

@@ -57,7 +57,7 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
const router = useRouter(); const router = useRouter();
const segments = useSegments(); const segments = useSegments();
const { showActionSheetWithOptions } = useActionSheet(); const { showActionSheetWithOptions } = useActionSheet();
const markAsPlayedStatus = useMarkAsPlayed(item); const markAsPlayedStatus = useMarkAsPlayed([item]);
const from = segments[2]; const from = segments[2];

View File

@@ -17,7 +17,9 @@ import {
SeasonIndexState, SeasonIndexState,
} from "@/components/series/SeasonDropdown"; } from "@/components/series/SeasonDropdown";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { PlayedStatus } from "../PlayedStatus";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
type Props = { type Props = {
item: BaseItemDto; item: BaseItemDto;
initialSeasonIndex?: number; initialSeasonIndex?: number;
@@ -145,17 +147,20 @@ export const SeasonPicker: React.FC<Props> = ({ item, initialSeasonIndex }) => {
}} }}
/> />
{episodes?.length || 0 > 0 ? ( {episodes?.length || 0 > 0 ? (
<DownloadItems <View className="flex flex-row items-center space-x-2">
title={t("item_card.download.download_season")} <DownloadItems
className="ml-2" title={t("item_card.download.download_season")}
items={episodes || []} className="ml-2"
MissingDownloadIconComponent={() => ( items={episodes || []}
<Ionicons name="download" size={20} color="white" /> MissingDownloadIconComponent={() => (
)} <Ionicons name="download" size={20} color="white" />
DownloadedIconComponent={() => ( )}
<Ionicons name="download" size={20} color="#9333ea" /> DownloadedIconComponent={() => (
)} <Ionicons name="download" size={20} color="#9333ea" />
/> )}
/>
<PlayedStatus items={episodes || []} />
</View>
) : null} ) : null}
</View> </View>
<View className="px-4 flex flex-col mt-4"> <View className="px-4 flex flex-col mt-4">

View File

@@ -6,7 +6,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useHaptic } from "./useHaptic"; import { useHaptic } from "./useHaptic";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
export const useMarkAsPlayed = (item: BaseItemDto) => { export const useMarkAsPlayed = (items: BaseItemDto[]) => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -14,7 +14,6 @@ export const useMarkAsPlayed = (item: BaseItemDto) => {
const invalidateQueries = () => { const invalidateQueries = () => {
const queriesToInvalidate = [ const queriesToInvalidate = [
["item", item.Id],
["resumeItems"], ["resumeItems"],
["continueWatching"], ["continueWatching"],
["nextUp-all"], ["nextUp-all"],
@@ -24,6 +23,11 @@ export const useMarkAsPlayed = (item: BaseItemDto) => {
["home"], ["home"],
]; ];
items.forEach((item) => {
if(!item.Id) return;
queriesToInvalidate.push(["item", item.Id]);
});
queriesToInvalidate.forEach((queryKey) => { queriesToInvalidate.forEach((queryKey) => {
queryClient.invalidateQueries({ queryKey }); queryClient.invalidateQueries({ queryKey });
}); });
@@ -32,40 +36,8 @@ export const useMarkAsPlayed = (item: BaseItemDto) => {
const markAsPlayedStatus = async (played: boolean) => { const markAsPlayedStatus = async (played: boolean) => {
lightHapticFeedback(); lightHapticFeedback();
// Optimistic update items.forEach((item) => {
queryClient.setQueryData( // Optimistic update
["item", item.Id],
(oldData: BaseItemDto | undefined) => {
if (oldData) {
return {
...oldData,
UserData: {
...oldData.UserData,
Played: !played,
},
};
}
return oldData;
}
);
try {
if (played) {
await markAsPlayed({
api: api,
item: item,
userId: user?.Id,
});
} else {
await markAsNotPlayed({
api: api,
itemId: item?.Id,
userId: user?.Id,
});
}
invalidateQueries();
} catch (error) {
// Revert optimistic update on error
queryClient.setQueryData( queryClient.setQueryData(
["item", item.Id], ["item", item.Id],
(oldData: BaseItemDto | undefined) => { (oldData: BaseItemDto | undefined) => {
@@ -81,8 +53,45 @@ export const useMarkAsPlayed = (item: BaseItemDto) => {
return oldData; return oldData;
} }
); );
})
try {
// Process all items
await Promise.all(items.map(item =>
played
? markAsPlayed({ api, item, userId: user?.Id })
: markAsNotPlayed({ api, itemId: item?.Id, userId: user?.Id })
));
// Bulk invalidate
queryClient.invalidateQueries({
queryKey: [
"resumeItems",
"continueWatching",
"nextUp-all",
"nextUp",
"episodes",
"seasons",
"home",
...items.map(item => ["item", item.Id])
].flat()
});
} catch (error) {
// Revert all optimistic updates on any failure
items.forEach(item => {
queryClient.setQueryData(
["item", item.Id],
(oldData: BaseItemDto | undefined) =>
oldData ? {
...oldData,
UserData: { ...oldData.UserData, Played: played }
} : oldData
);
});
console.error("Error updating played status:", error); console.error("Error updating played status:", error);
} }
invalidateQueries();
}; };
return markAsPlayedStatus; return markAsPlayedStatus;