diff --git a/apps/native/app.json b/apps/native/app.json index 4db051f..6a34038 100644 --- a/apps/native/app.json +++ b/apps/native/app.json @@ -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", { diff --git a/apps/native/src/app/person/[id].tsx b/apps/native/src/app/person/[id].tsx index d3f0697..7bbf7d6 100644 --- a/apps/native/src/app/person/[id].tsx +++ b/apps/native/src/app/person/[id].tsx @@ -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} /> ), - [ - columnWidth, - userStatuses, - handleQuickAdd, - addingKey, - failedKey, - resetError, - ], + [columnWidth, userStatuses, handleQuickAdd, addingKey], ); if (isPending) { diff --git a/apps/native/src/components/dashboard/continue-watching-card.tsx b/apps/native/src/components/dashboard/continue-watching-card.tsx index 87ae9bc..41f0289 100644 --- a/apps/native/src/components/dashboard/continue-watching-card.tsx +++ b/apps/native/src/components/dashboard/continue-watching-card.tsx @@ -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 ( - - - - - - {(item.nextEpisode?.stillPath || item.title.backdropPath) && ( - - )} + + + + + - {item.nextEpisode && ( - + className="overflow-hidden rounded-[12px] border bg-card" + style={{ + borderColor: "rgba(255,255,255,0.06)", + borderCurve: "continuous", + }} + > + + {(item.nextEpisode?.stillPath || item.title.backdropPath) && ( + + )} + + {item.nextEpisode && ( + + + S{item.nextEpisode.seasonNumber} E + {item.nextEpisode.episodeNumber} + + + )} + + 0 ? (item.watchedEpisodes / item.totalEpisodes) * 100 : 0}%`, + }} + /> + + + - S{item.nextEpisode.seasonNumber} E - {item.nextEpisode.episodeNumber} + {item.title.title} + {item.nextEpisode && ( + + {item.nextEpisode.name} + + )} - )} - - 0 ? (item.watchedEpisodes / item.totalEpisodes) * 100 : 0}%`, - }} - /> - - - - {item.title.title} - - {item.nextEpisode && ( - - {item.nextEpisode.name} - - )} - - - - - - + + + + + { + 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(), + }); + }} + /> + { + 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(), + }); + }} + /> + + + + ); } diff --git a/apps/native/src/components/dashboard/horizontal-poster-row.tsx b/apps/native/src/components/dashboard/horizontal-poster-row.tsx index d28b0db..671fe6f 100644 --- a/apps/native/src/components/dashboard/horizontal-poster-row.tsx +++ b/apps/native/src/components/dashboard/horizontal-poster-row.tsx @@ -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) { diff --git a/apps/native/src/components/explore/hero-banner.ios.tsx b/apps/native/src/components/explore/hero-banner.ios.tsx index 16a82bb..d0290e0 100644 --- a/apps/native/src/components/explore/hero-banner.ios.tsx +++ b/apps/native/src/components/explore/hero-banner.ios.tsx @@ -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 }) { + + { + 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(), + }); + }} + /> + diff --git a/apps/native/src/components/explore/hero-banner.tsx b/apps/native/src/components/explore/hero-banner.tsx index bd19027..820aad8 100644 --- a/apps/native/src/components/explore/hero-banner.tsx +++ b/apps/native/src/components/explore/hero-banner.tsx @@ -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 }) { + + { + 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(), + }); + }} + /> + diff --git a/apps/native/src/components/ui/poster-card.tsx b/apps/native/src/components/ui/poster-card.tsx index 53bece7..f0197fe 100644 --- a/apps/native/src/components/ui/poster-card.tsx +++ b/apps/native/src/components/ui/poster-card.tsx @@ -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( - 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 = ( @@ -153,15 +133,15 @@ export function PosterCard({ )} {/* Status indicator */} - {localStatus && ( + {userStatus && ( - {localStatus === "completed" ? ( + {userStatus === "completed" ? ( - ) : localStatus === "in_progress" ? ( + ) : userStatus === "in_progress" ? ( ) : ( @@ -190,10 +170,10 @@ export function PosterCard({ {/* Metadata */} - {localStatus && ( + {userStatus && ( )} ); - const quickAddButton = !localStatus ? ( + const quickAddButton = !userStatus ? ( - - - - - - - - {cardContent} - - - - - {quickAddButton} - - - - - - {!localStatus && ( - - - Add to Watchlist - - )} - {localStatus !== "in_progress" && ( - { - 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(), - }); - }} - > - - Mark as Watching - - )} - {type === "movie" && ( - { - 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(), - }); - }} - > - - Mark as Watched - - )} - {localStatus && ( - { - 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(), - }); - }} - > - - Remove from Library - - )} - - + + + + + + + {cardContent} + + + + + {!userStatus && ( + + )} + {userStatus !== "in_progress" && ( + { + 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" && ( + { + 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 && ( + { + await client.titles.updateStatus({ id, status: null }); + toast.success("Removed from library"); + queryClient.invalidateQueries({ + queryKey: orpc.titles.key(), + }); + queryClient.invalidateQueries({ + queryKey: orpc.dashboard.key(), + }); + }} + /> + )} + + + {quickAddButton} + + + ); } diff --git a/apps/native/src/hooks/use-poster-actions.ts b/apps/native/src/hooks/use-poster-actions.ts index b2fc017..559db48 100644 --- a/apps/native/src/hooks/use-poster-actions.ts +++ b/apps/native/src/hooks/use-poster-actions.ts @@ -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 }; }