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