refactor(native): align title action buttons with web app design

Replace three separate status toggle buttons (Watchlist/Watching/Completed)
with a single status button matching the web pattern: "+ Watchlist" when
inactive, "Watching"/"Completed" when active (tap to remove). Consolidate
actions into a single row with separator and star rating. Use title-accent
color for theming consistency with other native components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 17:55:01 -04:00
co-authored by Claude Opus 4.6
parent c037a64028
commit baf203516a
2 changed files with 94 additions and 89 deletions
+31 -47
View File
@@ -104,17 +104,6 @@ export default function TitleDetailScreen() {
}),
);
const watchAll = useMutation(
orpc.titles.watchAll.mutationOptions({
onSuccess: () => {
toast.success("Marked all episodes as watched");
queryClient.invalidateQueries({ queryKey: orpc.titles.key() });
queryClient.invalidateQueries({ queryKey: orpc.dashboard.key() });
},
onError: () => toast.error("Failed to mark all episodes as watched"),
}),
);
const quickAddMutation = useMutation(
orpc.titles.quickAdd.mutationOptions({
onSuccess: () => {
@@ -193,10 +182,10 @@ export default function TitleDetailScreen() {
<Skeleton width={50} height={24} borderRadius={12} />
</View>
{/* Actions skeleton */}
<View className="mt-4 flex-row gap-2 px-4">
<Skeleton width={100} height={36} borderRadius={18} />
<Skeleton width={90} height={36} borderRadius={18} />
<Skeleton width={105} height={36} borderRadius={18} />
<View className="mt-4 flex-row items-center gap-3 px-4">
<Skeleton width={110} height={36} borderRadius={8} />
<Skeleton width={1} height={24} borderRadius={0} />
<Skeleton width={120} height={22} borderRadius={4} />
</View>
{/* Overview skeleton */}
<View className="mt-5 gap-2 px-4">
@@ -415,57 +404,52 @@ export default function TitleDetailScreen() {
entering={FadeInDown.duration(300).delay(200)}
className="mt-4 px-4"
>
<StatusActionButton
currentStatus={userInfo.data?.status ?? null}
onStatusChange={(status) => {
if (status === null) {
updateStatus.mutate({ id, status: null });
} else if (status === "watchlist") {
quickAddMutation.mutate({
tmdbId: title.tmdbId,
type: title.type,
});
} else if (status === "completed" && title.type === "movie") {
watchMovie.mutate({ id });
} else if (status === "completed" && title.type === "tv") {
watchAll.mutate({ id });
} else {
updateStatus.mutate({ id, status });
<View className="flex-row flex-wrap items-center gap-3">
<StatusActionButton
currentStatus={userInfo.data?.status ?? null}
onStatusChange={(status) => {
if (status === "watchlist") {
quickAddMutation.mutate({
tmdbId: title.tmdbId,
type: title.type,
});
} else {
updateStatus.mutate({ id, status: null });
}
}}
isPending={
updateStatus.isPending ||
quickAddMutation.isPending ||
watchMovie.isPending
}
}}
isPending={
updateStatus.isPending ||
quickAddMutation.isPending ||
watchMovie.isPending ||
watchAll.isPending
}
/>
<View className="mt-4 flex-row items-center justify-between">
<StarRating
rating={userInfo.data?.rating ?? 0}
onRate={(stars) => updateRating.mutate({ id, stars })}
accentColor={titleAccent}
/>
{title.type === "movie" && (
<Pressable
onPress={() => watchMovie.mutate({ id })}
disabled={watchMovie.isPending}
className="flex-row items-center gap-1.5 rounded-full bg-title-accent px-4 py-2"
className="flex-row items-center gap-1.5 rounded-lg bg-title-accent px-4 py-2"
>
{watchMovie.isPending ? (
<Spinner size="sm" />
) : (
<>
<IconCheck size={16} color={titleAccentForeground} />
<Text className="font-sans-medium text-[13px] text-title-accent-foreground">
<Text className="font-sans-medium text-sm text-title-accent-foreground">
Mark Watched
</Text>
</>
)}
</Pressable>
)}
<View className="h-6 w-px bg-border/50" />
<StarRating
rating={userInfo.data?.rating ?? 0}
onRate={(stars) => updateRating.mutate({ id, stars })}
accentColor={titleAccent}
/>
</View>
</Animated.View>
@@ -1,15 +1,33 @@
import {
IconBookmark,
IconCircleCheck,
IconPlayerPlay,
IconCheck,
IconPlayerPlayFilled,
IconPlus,
} from "@tabler/icons-react-native";
import { Pressable, View } from "react-native";
import { Pressable } from "react-native";
import { useCSSVariable } from "uniwind";
import { Text } from "@/components/ui/text";
import * as Haptics from "@/utils/haptics";
type TitleStatus = "watchlist" | "in_progress" | "completed";
const watchingStyle = {
label: "Watching",
Icon: IconPlayerPlayFilled,
bgClass: "bg-title-accent/10 border-title-accent/20",
textClass: "text-title-accent",
};
const statusConfig = {
watchlist: watchingStyle,
in_progress: watchingStyle,
completed: {
label: "Completed",
Icon: IconCheck,
bgClass: "bg-status-completed/10 border-status-completed/20",
textClass: "text-status-completed",
},
} as const;
export function StatusActionButton({
currentStatus,
onStatusChange,
@@ -19,46 +37,49 @@ export function StatusActionButton({
onStatusChange: (status: TitleStatus | null) => void;
isPending: boolean;
}) {
const statuses: Array<{
status: TitleStatus;
label: string;
Icon: typeof IconBookmark;
}> = [
{ status: "watchlist", label: "Watchlist", Icon: IconBookmark },
{ status: "in_progress", label: "Watching", Icon: IconPlayerPlay },
{ status: "completed", label: "Completed", Icon: IconCircleCheck },
];
const [titleAccent, completedColor] = useCSSVariable([
"--color-title-accent",
"--color-status-completed",
]) as [string, string];
const primaryColor = useCSSVariable("--color-title-accent") as string;
const mutedFgColor = useCSSVariable("--color-muted-foreground") as string;
const config = currentStatus
? statusConfig[currentStatus as keyof typeof statusConfig]
: null;
if (!config) {
return (
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
onStatusChange("watchlist");
}}
disabled={isPending}
className="flex-row items-center gap-1.5 rounded-lg border border-title-accent/20 bg-title-accent/10 px-4 py-2"
>
<IconPlus size={14} color={titleAccent} strokeWidth={2.5} />
<Text className="font-sans-medium text-sm text-title-accent">
Watchlist
</Text>
</Pressable>
);
}
const iconColor =
currentStatus === "completed" ? completedColor : titleAccent;
return (
<View className="flex-row gap-2">
{statuses.map(({ status, label, Icon }) => {
const isActive = currentStatus === status;
return (
<Pressable
key={status}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
onStatusChange(isActive ? null : status);
}}
disabled={isPending}
className={`flex-row items-center gap-1.5 rounded-full border px-3 py-2 ${
isActive
? "border-title-accent bg-title-accent/10"
: "border-border bg-card"
}`}
>
<Icon size={14} color={isActive ? primaryColor : mutedFgColor} />
<Text
className={`font-sans-medium text-xs ${isActive ? "text-title-accent" : "text-foreground"}`}
>
{label}
</Text>
</Pressable>
);
})}
</View>
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
onStatusChange(null);
}}
disabled={isPending}
className={`flex-row items-center gap-1.5 rounded-lg border px-4 py-2 ${config.bgClass}`}
>
<config.Icon size={14} color={iconColor} />
<Text className={`font-sans-medium text-sm ${config.textClass}`}>
{config.label}
</Text>
</Pressable>
);
}