mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-31 19:18:26 +01:00
feat: Mark entire seasons of a show as played (#445)
This commit is contained in:
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user