mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
feat(native): add context menus to poster cards, hero banners, and continue-watching cards; remove failedKey error state
- Add `Link.Menu` with contextual actions to `PosterCard` (add to watchlist, mark completed, remove), `HeroBanner` ("Add to Watchlist"), and `ContinueWatchingCard` ("Mark as Completed", "Remove from Library") — all invalidate the relevant `titles` and `dashboard` query keys on success
- Remove the `failedKey`/`resetError` error-state pattern from `usePosterActions` and strip the now-unused `failedKey` and `onQuickAddFailed` props from every call site (`HorizontalPosterRow`, `person/[id].tsx`, `PosterCard`)
- Restructure `ContinueWatchingCard` JSX so `GestureDetector` wraps the outer `Animated.View` rather than sitting inside `Link`, fixing gesture/preview layering
- Simplify single-argument plugin entries in `app.json` from `["plugin-name"]` to `"plugin-name"`
This commit is contained in:
+5
-15
@@ -23,18 +23,8 @@
|
||||
"package": "com.jakejarvis.sofa"
|
||||
},
|
||||
"plugins": [
|
||||
[
|
||||
"expo-router",
|
||||
{
|
||||
"root": "./src/app"
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-secure-store",
|
||||
{
|
||||
"configureAndroidBackup": true
|
||||
}
|
||||
],
|
||||
"expo-router",
|
||||
"expo-secure-store",
|
||||
[
|
||||
"expo-font",
|
||||
{
|
||||
@@ -101,15 +91,15 @@
|
||||
"imageWidth": 200
|
||||
}
|
||||
],
|
||||
["expo-system-ui"],
|
||||
["expo-web-browser"],
|
||||
"expo-system-ui",
|
||||
"expo-web-browser",
|
||||
[
|
||||
"expo-image-picker",
|
||||
{
|
||||
"microphonePermission": false
|
||||
}
|
||||
],
|
||||
["expo-localization"],
|
||||
"expo-localization",
|
||||
[
|
||||
"expo-tracking-transparency",
|
||||
{
|
||||
|
||||
@@ -75,8 +75,7 @@ export default function PersonDetailScreen() {
|
||||
const mutedForeground = useCSSVariable("--color-muted-foreground") as string;
|
||||
const primaryColor = useCSSVariable("--color-primary") as string;
|
||||
|
||||
const { handleQuickAdd, addingKey, failedKey, resetError } =
|
||||
usePosterActions();
|
||||
const { handleQuickAdd, addingKey } = usePosterActions();
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -144,19 +143,10 @@ export default function PersonDetailScreen() {
|
||||
width={columnWidth}
|
||||
onQuickAdd={handleQuickAdd}
|
||||
isAdding={addingKey === credit.titleId}
|
||||
failedKey={failedKey}
|
||||
onQuickAddFailed={resetError}
|
||||
/>
|
||||
</View>
|
||||
),
|
||||
[
|
||||
columnWidth,
|
||||
userStatuses,
|
||||
handleQuickAdd,
|
||||
addingKey,
|
||||
failedKey,
|
||||
resetError,
|
||||
],
|
||||
[columnWidth, userStatuses, handleQuickAdd, addingKey],
|
||||
);
|
||||
|
||||
if (isPending) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link } from "expo-router";
|
||||
import { View } from "react-native";
|
||||
import { Pressable, View } from "react-native";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import Animated, {
|
||||
interpolate,
|
||||
@@ -10,6 +10,9 @@ import Animated, {
|
||||
} from "react-native-reanimated";
|
||||
import { Image } from "@/components/ui/image";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { client, orpc } from "@/lib/orpc";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
export interface ContinueWatchingItem {
|
||||
title: {
|
||||
@@ -57,85 +60,123 @@ export function ContinueWatchingCard({ item }: { item: ContinueWatchingItem }) {
|
||||
.join(", ");
|
||||
|
||||
return (
|
||||
<Link href={`/title/${item.title.id}` as `/title/${string}`} asChild>
|
||||
<Link.Trigger withAppleZoom>
|
||||
<GestureDetector gesture={tapGesture}>
|
||||
<Animated.View
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={cardLabel}
|
||||
className="w-[200px] overflow-hidden rounded-[12px] border bg-card"
|
||||
style={[
|
||||
animatedStyle,
|
||||
{
|
||||
borderColor: "rgba(255,255,255,0.06)",
|
||||
borderCurve: "continuous",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View className="h-28 w-[200px]">
|
||||
{(item.nextEpisode?.stillPath || item.title.backdropPath) && (
|
||||
<Image
|
||||
source={{
|
||||
uri: (item.nextEpisode?.stillPath ??
|
||||
item.title.backdropPath) as string,
|
||||
}}
|
||||
thumbHash={
|
||||
item.nextEpisode?.stillThumbHash ??
|
||||
item.title.backdropThumbHash
|
||||
}
|
||||
recyclingKey={item.title.id}
|
||||
className="h-full w-full"
|
||||
contentFit="cover"
|
||||
/>
|
||||
)}
|
||||
<GestureDetector gesture={tapGesture}>
|
||||
<Animated.View className="w-[200px]" style={animatedStyle}>
|
||||
<Link href={`/title/${item.title.id}` as `/title/${string}`} asChild>
|
||||
<Link.Trigger withAppleZoom>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={cardLabel}
|
||||
>
|
||||
<View
|
||||
className="absolute inset-0"
|
||||
style={{ backgroundColor: "rgba(0,0,0,0.4)" }}
|
||||
/>
|
||||
{item.nextEpisode && (
|
||||
<View className="absolute right-2.5 bottom-3 left-2.5">
|
||||
className="overflow-hidden rounded-[12px] border bg-card"
|
||||
style={{
|
||||
borderColor: "rgba(255,255,255,0.06)",
|
||||
borderCurve: "continuous",
|
||||
}}
|
||||
>
|
||||
<View className="h-28 w-[200px]">
|
||||
{(item.nextEpisode?.stillPath || item.title.backdropPath) && (
|
||||
<Image
|
||||
source={{
|
||||
uri: (item.nextEpisode?.stillPath ??
|
||||
item.title.backdropPath) as string,
|
||||
}}
|
||||
thumbHash={
|
||||
item.nextEpisode?.stillThumbHash ??
|
||||
item.title.backdropThumbHash
|
||||
}
|
||||
recyclingKey={item.title.id}
|
||||
className="h-full w-full"
|
||||
contentFit="cover"
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
className="absolute inset-0"
|
||||
style={{ backgroundColor: "rgba(0,0,0,0.4)" }}
|
||||
/>
|
||||
{item.nextEpisode && (
|
||||
<View className="absolute right-2.5 bottom-3 left-2.5">
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
className="font-medium font-sans text-white/80 text-xs"
|
||||
>
|
||||
S{item.nextEpisode.seasonNumber} E
|
||||
{item.nextEpisode.episodeNumber}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
className="absolute right-0 bottom-0 left-0 h-[3px]"
|
||||
style={{ backgroundColor: "rgba(255,255,255,0.1)" }}
|
||||
>
|
||||
<View
|
||||
className="h-full bg-status-watching"
|
||||
style={{
|
||||
width: `${item.totalEpisodes > 0 ? (item.watchedEpisodes / item.totalEpisodes) * 100 : 0}%`,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View className="p-2.5">
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
className="font-medium font-sans text-white/80 text-xs"
|
||||
className="font-medium font-sans text-foreground text-sm"
|
||||
>
|
||||
S{item.nextEpisode.seasonNumber} E
|
||||
{item.nextEpisode.episodeNumber}
|
||||
{item.title.title}
|
||||
</Text>
|
||||
{item.nextEpisode && (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
className="mt-0.5 text-muted-foreground text-xs"
|
||||
>
|
||||
{item.nextEpisode.name}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
className="absolute right-0 bottom-0 left-0 h-[3px]"
|
||||
style={{ backgroundColor: "rgba(255,255,255,0.1)" }}
|
||||
>
|
||||
<View
|
||||
className="h-full bg-status-watching"
|
||||
style={{
|
||||
width: `${item.totalEpisodes > 0 ? (item.watchedEpisodes / item.totalEpisodes) * 100 : 0}%`,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View className="p-2.5">
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
className="font-medium font-sans text-foreground text-sm"
|
||||
>
|
||||
{item.title.title}
|
||||
</Text>
|
||||
{item.nextEpisode && (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
className="mt-0.5 text-muted-foreground text-xs"
|
||||
>
|
||||
{item.nextEpisode.name}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
</Link.Trigger>
|
||||
<Link.Preview />
|
||||
</Link>
|
||||
</Pressable>
|
||||
</Link.Trigger>
|
||||
<Link.Preview />
|
||||
<Link.Menu>
|
||||
<Link.MenuAction
|
||||
title="Mark as Completed"
|
||||
icon="checkmark.circle"
|
||||
onPress={async () => {
|
||||
await client.titles.updateStatus({
|
||||
id: item.title.id,
|
||||
status: "completed",
|
||||
});
|
||||
toast.success(`Marked "${item.title.title}" as completed`);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.titles.key(),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.dashboard.key(),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Link.MenuAction
|
||||
title="Remove from Library"
|
||||
icon="trash"
|
||||
destructive
|
||||
onPress={async () => {
|
||||
await client.titles.updateStatus({
|
||||
id: item.title.id,
|
||||
status: null,
|
||||
});
|
||||
toast.success("Removed from library");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.titles.key(),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.dashboard.key(),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Link.Menu>
|
||||
</Link>
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,8 +29,7 @@ export function HorizontalPosterRow({
|
||||
items: PosterRowItem[];
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
const { handleQuickAdd, addingKey, failedKey, resetError } =
|
||||
usePosterActions();
|
||||
const { handleQuickAdd, addingKey } = usePosterActions();
|
||||
const keyExtractor = useCallback((item: PosterRowItem) => item.id, []);
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: PosterRowItem }) => (
|
||||
@@ -46,11 +45,9 @@ export function HorizontalPosterRow({
|
||||
episodeProgress={item.episodeProgress}
|
||||
onQuickAdd={handleQuickAdd}
|
||||
isAdding={addingKey === item.id}
|
||||
failedKey={failedKey}
|
||||
onQuickAddFailed={resetError}
|
||||
/>
|
||||
),
|
||||
[addingKey, failedKey, handleQuickAdd, resetError],
|
||||
[addingKey, handleQuickAdd],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
|
||||
@@ -13,6 +13,9 @@ import Animated, {
|
||||
import { useCSSVariable } from "uniwind";
|
||||
import { Image } from "@/components/ui/image";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { client, orpc } from "@/lib/orpc";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
export interface HeroBannerItem {
|
||||
id: string;
|
||||
@@ -161,6 +164,22 @@ export function HeroBanner({ item }: { item: HeroBannerItem }) {
|
||||
</Pressable>
|
||||
</Link.Trigger>
|
||||
<Link.Preview />
|
||||
<Link.Menu>
|
||||
<Link.MenuAction
|
||||
title="Add to Watchlist"
|
||||
icon="bookmark"
|
||||
onPress={async () => {
|
||||
await client.titles.quickAdd({ id: item.id });
|
||||
toast.success(`Added "${item.title}" to watchlist`);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.titles.key(),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.dashboard.key(),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Link.Menu>
|
||||
</Link>
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
|
||||
@@ -13,6 +13,9 @@ import { useCSSVariable } from "uniwind";
|
||||
import { Image } from "@/components/ui/image";
|
||||
import { ScaledIcon } from "@/components/ui/scaled-icon";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { client, orpc } from "@/lib/orpc";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
export interface HeroBannerItem {
|
||||
id: string;
|
||||
@@ -121,6 +124,22 @@ export function HeroBanner({ item }: { item: HeroBannerItem }) {
|
||||
</Pressable>
|
||||
</Link.Trigger>
|
||||
<Link.Preview />
|
||||
<Link.Menu>
|
||||
<Link.MenuAction
|
||||
title="Add to Watchlist"
|
||||
icon="bookmark"
|
||||
onPress={async () => {
|
||||
await client.titles.quickAdd({ id: item.id });
|
||||
toast.success(`Added "${item.title}" to watchlist`);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.titles.key(),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.dashboard.key(),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Link.Menu>
|
||||
</Link>
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
IconStarFilled,
|
||||
} from "@tabler/icons-react-native";
|
||||
import { Link } from "expo-router";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { Pressable, View } from "react-native";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import Animated, {
|
||||
@@ -20,7 +20,6 @@ import Animated, {
|
||||
withSpring,
|
||||
} from "react-native-reanimated";
|
||||
import { useCSSVariable } from "uniwind";
|
||||
import * as ContextMenu from "zeego/context-menu";
|
||||
import { Image } from "@/components/ui/image";
|
||||
import { ScaledIcon } from "@/components/ui/scaled-icon";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -44,8 +43,6 @@ interface PosterCardProps {
|
||||
width?: number;
|
||||
onQuickAdd: (id: string) => void;
|
||||
isAdding?: boolean;
|
||||
failedKey?: string | null;
|
||||
onQuickAddFailed?: () => void;
|
||||
}
|
||||
|
||||
export function PosterCard({
|
||||
@@ -61,8 +58,6 @@ export function PosterCard({
|
||||
width = 140,
|
||||
onQuickAdd,
|
||||
isAdding,
|
||||
failedKey,
|
||||
onQuickAddFailed,
|
||||
}: PosterCardProps) {
|
||||
const primaryColor = useCSSVariable("--color-primary") as string;
|
||||
const watchlistColor = useCSSVariable("--color-status-watchlist") as string;
|
||||
@@ -77,20 +72,6 @@ export function PosterCard({
|
||||
|
||||
const reduceMotion = useReducedMotion();
|
||||
const pressed = useSharedValue(0);
|
||||
const [localStatus, setLocalStatus] = useState<TitleStatus | null>(
|
||||
userStatus ?? null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalStatus(userStatus ?? null);
|
||||
}, [userStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (failedKey === id) {
|
||||
setLocalStatus(userStatus ?? null);
|
||||
onQuickAddFailed?.();
|
||||
}
|
||||
}, [failedKey, id, userStatus, onQuickAddFailed]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
@@ -101,20 +82,19 @@ export function PosterCard({
|
||||
}));
|
||||
|
||||
const handleQuickAddPress = useCallback(() => {
|
||||
if (localStatus || isAdding) return;
|
||||
setLocalStatus("watchlist");
|
||||
if (userStatus || isAdding) return;
|
||||
onQuickAdd(id);
|
||||
}, [localStatus, isAdding, onQuickAdd, id]);
|
||||
}, [userStatus, isAdding, onQuickAdd, id]);
|
||||
|
||||
const year = releaseDate?.slice(0, 4);
|
||||
const imageHeight = width * 1.5;
|
||||
|
||||
const statusLabel =
|
||||
localStatus === "completed"
|
||||
userStatus === "completed"
|
||||
? "completed"
|
||||
: localStatus === "in_progress"
|
||||
: userStatus === "in_progress"
|
||||
? "watching"
|
||||
: localStatus === "watchlist"
|
||||
: userStatus === "watchlist"
|
||||
? "on watchlist"
|
||||
: undefined;
|
||||
const cardAccessibilityLabel = [
|
||||
@@ -129,7 +109,7 @@ export function PosterCard({
|
||||
const cardContent = (
|
||||
<View
|
||||
className={`overflow-hidden rounded-xl border bg-card ${
|
||||
localStatus ? "border-primary/25" : "border-white/[0.06]"
|
||||
userStatus ? "border-primary/25" : "border-white/[0.06]"
|
||||
}`}
|
||||
style={{ borderCurve: "continuous" }}
|
||||
>
|
||||
@@ -153,15 +133,15 @@ export function PosterCard({
|
||||
)}
|
||||
|
||||
{/* Status indicator */}
|
||||
{localStatus && (
|
||||
{userStatus && (
|
||||
<View
|
||||
accessible={false}
|
||||
className="absolute top-2 right-2 size-[30px] items-center justify-center rounded-full"
|
||||
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
|
||||
>
|
||||
{localStatus === "completed" ? (
|
||||
{userStatus === "completed" ? (
|
||||
<IconCheckbox size={16} color="white" />
|
||||
) : localStatus === "in_progress" ? (
|
||||
) : userStatus === "in_progress" ? (
|
||||
<IconPlayerPlayFilled size={16} color="white" />
|
||||
) : (
|
||||
<IconBookmarkFilled size={16} color="white" />
|
||||
@@ -190,10 +170,10 @@ export function PosterCard({
|
||||
{/* Metadata */}
|
||||
<View className="px-2.5 pt-2 pb-2.5">
|
||||
<View className="flex-row items-center gap-1.5">
|
||||
{localStatus && (
|
||||
{userStatus && (
|
||||
<View
|
||||
className="size-1.5 rounded-full"
|
||||
style={{ backgroundColor: statusColors[localStatus] }}
|
||||
style={{ backgroundColor: statusColors[userStatus] }}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
@@ -228,7 +208,7 @@ export function PosterCard({
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
const quickAddButton = !localStatus ? (
|
||||
const quickAddButton = !userStatus ? (
|
||||
<Pressable
|
||||
onPress={handleQuickAddPress}
|
||||
disabled={!!isAdding}
|
||||
@@ -262,97 +242,89 @@ export function PosterCard({
|
||||
const titleHref = `/title/${id}` as `/title/${string}`;
|
||||
|
||||
return (
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>
|
||||
<GestureDetector gesture={pressGesture}>
|
||||
<Animated.View style={[animatedStyle, { width }]}>
|
||||
<View>
|
||||
<Link href={titleHref} asChild>
|
||||
<Link.Trigger withAppleZoom>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={cardAccessibilityLabel}
|
||||
>
|
||||
{cardContent}
|
||||
</Pressable>
|
||||
</Link.Trigger>
|
||||
<Link.Preview />
|
||||
</Link>
|
||||
{quickAddButton}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Content>
|
||||
{!localStatus && (
|
||||
<ContextMenu.Item key="watchlist" onSelect={handleQuickAddPress}>
|
||||
<ContextMenu.ItemIcon ios={{ name: "bookmark" }} />
|
||||
<ContextMenu.ItemTitle>Add to Watchlist</ContextMenu.ItemTitle>
|
||||
</ContextMenu.Item>
|
||||
)}
|
||||
{localStatus !== "in_progress" && (
|
||||
<ContextMenu.Item
|
||||
key="watching"
|
||||
onSelect={async () => {
|
||||
await client.titles.updateStatus({
|
||||
id,
|
||||
status: "in_progress",
|
||||
});
|
||||
toast.success("Marked as watching");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.titles.key(),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.dashboard.key(),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ContextMenu.ItemIcon ios={{ name: "play.fill" }} />
|
||||
<ContextMenu.ItemTitle>Mark as Watching</ContextMenu.ItemTitle>
|
||||
</ContextMenu.Item>
|
||||
)}
|
||||
{type === "movie" && (
|
||||
<ContextMenu.Item
|
||||
key="watched"
|
||||
onSelect={async () => {
|
||||
await client.titles.watchMovie({ id });
|
||||
toast.success(
|
||||
title ? `Marked "${title}" as watched` : "Marked as watched",
|
||||
);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.titles.key(),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.dashboard.key(),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ContextMenu.ItemIcon ios={{ name: "checkmark.circle" }} />
|
||||
<ContextMenu.ItemTitle>Mark as Watched</ContextMenu.ItemTitle>
|
||||
</ContextMenu.Item>
|
||||
)}
|
||||
{localStatus && (
|
||||
<ContextMenu.Item
|
||||
key="remove"
|
||||
destructive
|
||||
onSelect={async () => {
|
||||
await client.titles.updateStatus({ id, status: null });
|
||||
setLocalStatus(null);
|
||||
toast.success("Removed from library");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.titles.key(),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.dashboard.key(),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ContextMenu.ItemIcon ios={{ name: "trash" }} />
|
||||
<ContextMenu.ItemTitle>Remove from Library</ContextMenu.ItemTitle>
|
||||
</ContextMenu.Item>
|
||||
)}
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Root>
|
||||
<GestureDetector gesture={pressGesture}>
|
||||
<Animated.View style={[animatedStyle, { width }]}>
|
||||
<View>
|
||||
<Link href={titleHref} asChild>
|
||||
<Link.Trigger withAppleZoom>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={cardAccessibilityLabel}
|
||||
>
|
||||
{cardContent}
|
||||
</Pressable>
|
||||
</Link.Trigger>
|
||||
<Link.Preview />
|
||||
<Link.Menu>
|
||||
{!userStatus && (
|
||||
<Link.MenuAction
|
||||
title="Add to Watchlist"
|
||||
icon="bookmark"
|
||||
onPress={handleQuickAddPress}
|
||||
/>
|
||||
)}
|
||||
{userStatus !== "in_progress" && (
|
||||
<Link.MenuAction
|
||||
title="Mark as Watching"
|
||||
icon="play.fill"
|
||||
onPress={async () => {
|
||||
await client.titles.updateStatus({
|
||||
id,
|
||||
status: "in_progress",
|
||||
});
|
||||
toast.success("Marked as watching");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.titles.key(),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.dashboard.key(),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{type === "movie" && (
|
||||
<Link.MenuAction
|
||||
title="Mark as Watched"
|
||||
icon="checkmark.circle"
|
||||
onPress={async () => {
|
||||
await client.titles.watchMovie({ id });
|
||||
toast.success(
|
||||
title
|
||||
? `Marked "${title}" as watched`
|
||||
: "Marked as watched",
|
||||
);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.titles.key(),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.dashboard.key(),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{userStatus && (
|
||||
<Link.MenuAction
|
||||
title="Remove from Library"
|
||||
icon="trash"
|
||||
destructive
|
||||
onPress={async () => {
|
||||
await client.titles.updateStatus({ id, status: null });
|
||||
toast.success("Removed from library");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.titles.key(),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orpc.dashboard.key(),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Link.Menu>
|
||||
</Link>
|
||||
{quickAddButton}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useCallback } from "react";
|
||||
import { orpc } from "@/lib/orpc";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { toast } from "@/lib/toast";
|
||||
@@ -25,26 +24,14 @@ export function usePosterActions() {
|
||||
}),
|
||||
);
|
||||
|
||||
const handleQuickAdd = useCallback(
|
||||
(id: string) => {
|
||||
quickAddMutation.mutate({ id });
|
||||
},
|
||||
[quickAddMutation.mutate],
|
||||
);
|
||||
const handleQuickAdd = (id: string) => {
|
||||
quickAddMutation.mutate({ id });
|
||||
};
|
||||
|
||||
const addingKey =
|
||||
quickAddMutation.isPending && quickAddMutation.variables
|
||||
? quickAddMutation.variables.id
|
||||
: null;
|
||||
|
||||
const failedKey =
|
||||
quickAddMutation.isError && quickAddMutation.variables
|
||||
? quickAddMutation.variables.id
|
||||
: null;
|
||||
|
||||
const resetError = useCallback(() => {
|
||||
quickAddMutation.reset();
|
||||
}, [quickAddMutation.reset]);
|
||||
|
||||
return { handleQuickAdd, addingKey, failedKey, resetError };
|
||||
return { handleQuickAdd, addingKey };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user