mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
feat: add "Mark Unwatched" toggle for movies on web and native (#52)
- Replace the static "Mark Watched" button with a toggle that shows "Mark Unwatched" (destructive style) when a movie is already completed - Add `handleUnwatchMovie` to web `useTitleActions` with optimistic update (reverts status to `in_watchlist` on error) - Add `unwatchMovie` mutation to native `useTitleActions` hook and `titleActions` imperative helper - Update poster-card and upcoming-row context menus to show the correct action based on current watch status - Wire the `M` keyboard shortcut on the title detail page to unwatch when the movie is already marked watched - Add i18n strings for "Mark Unwatched", "Marked as unwatched", and "Failed to mark as unwatched"
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
|||||||
IconStarFilled,
|
IconStarFilled,
|
||||||
IconThumbUp,
|
IconThumbUp,
|
||||||
IconUsers,
|
IconUsers,
|
||||||
|
IconX,
|
||||||
} from "@tabler/icons-react-native";
|
} from "@tabler/icons-react-native";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect";
|
import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect";
|
||||||
@@ -98,22 +99,27 @@ export default function TitleDetailScreen() {
|
|||||||
const { back } = useRouter();
|
const { back } = useRouter();
|
||||||
const useAutomaticInsets = process.env.EXPO_OS === "ios";
|
const useAutomaticInsets = process.env.EXPO_OS === "ios";
|
||||||
|
|
||||||
const [titleAccent, mutedForeground, titleAccentForeground] = useCSSVariable([
|
const [titleAccent, mutedForeground, titleAccentForeground, destructiveColor] = useCSSVariable([
|
||||||
"--color-title-accent",
|
"--color-title-accent",
|
||||||
"--color-muted-foreground",
|
"--color-muted-foreground",
|
||||||
"--color-title-accent-foreground",
|
"--color-title-accent-foreground",
|
||||||
]) as [string, string, string];
|
"--color-destructive",
|
||||||
|
]) as [string, string, string, string];
|
||||||
|
|
||||||
const detail = useQuery(orpc.titles.get.queryOptions({ input: { id } }));
|
const detail = useQuery(orpc.titles.get.queryOptions({ input: { id } }));
|
||||||
const userInfo = useQuery(orpc.tracking.userInfo.queryOptions({ input: { id } }));
|
const userInfo = useQuery(orpc.tracking.userInfo.queryOptions({ input: { id } }));
|
||||||
const recommendations = useQuery(orpc.titles.similar.queryOptions({ input: { id } }));
|
const recommendations = useQuery(orpc.titles.similar.queryOptions({ input: { id } }));
|
||||||
|
|
||||||
const { updateStatus, updateRating, watchMovie } = useTitleActions({
|
const { updateStatus, updateRating, watchMovie, unwatchMovie } = useTitleActions({
|
||||||
toasts: {
|
toasts: {
|
||||||
watchMovie: () => {
|
watchMovie: () => {
|
||||||
const name = title?.title;
|
const name = title?.title;
|
||||||
return name ? t`Marked "${name}" as watched` : t`Marked as watched`;
|
return name ? t`Marked "${name}" as watched` : t`Marked as watched`;
|
||||||
},
|
},
|
||||||
|
unwatchMovie: () => {
|
||||||
|
const name = title?.title;
|
||||||
|
return name ? t`Marked "${name}" as unwatched` : t`Marked as unwatched`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -147,6 +153,7 @@ export default function TitleDetailScreen() {
|
|||||||
() => new Set(userInfo.data?.episodeWatches ?? []),
|
() => new Set(userInfo.data?.episodeWatches ?? []),
|
||||||
[userInfo.data?.episodeWatches],
|
[userInfo.data?.episodeWatches],
|
||||||
);
|
);
|
||||||
|
const isMovieWatched = userInfo.data?.status === "completed";
|
||||||
|
|
||||||
const recItems = useMemo<PosterRowItem[]>(
|
const recItems = useMemo<PosterRowItem[]>(
|
||||||
() =>
|
() =>
|
||||||
@@ -413,17 +420,34 @@ export default function TitleDetailScreen() {
|
|||||||
updateStatus.mutate({ id, status: null });
|
updateStatus.mutate({ id, status: null });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
isPending={updateStatus.isPending || watchMovie.isPending}
|
isPending={updateStatus.isPending || watchMovie.isPending || unwatchMovie.isPending}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{title.type === "movie" && (
|
{title.type === "movie" && (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => watchMovie.mutate({ scope: "movie", ids: [id] })}
|
onPress={() => {
|
||||||
disabled={watchMovie.isPending}
|
if (isMovieWatched) {
|
||||||
className="bg-title-accent flex-row items-center gap-1.5 rounded-lg px-4 py-2"
|
unwatchMovie.mutate({ scope: "movie", ids: [id] });
|
||||||
|
} else {
|
||||||
|
watchMovie.mutate({ scope: "movie", ids: [id] });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={watchMovie.isPending || unwatchMovie.isPending}
|
||||||
|
className={
|
||||||
|
isMovieWatched
|
||||||
|
? "border-destructive/20 bg-destructive/10 flex-row items-center gap-1.5 rounded-lg border px-4 py-2"
|
||||||
|
: "bg-title-accent flex-row items-center gap-1.5 rounded-lg px-4 py-2"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{watchMovie.isPending ? (
|
{watchMovie.isPending || unwatchMovie.isPending ? (
|
||||||
<Spinner size="sm" />
|
<Spinner size="sm" />
|
||||||
|
) : isMovieWatched ? (
|
||||||
|
<>
|
||||||
|
<ScaledIcon icon={IconX} size={16} color={destructiveColor} />
|
||||||
|
<Text className="text-destructive font-sans text-sm font-medium">
|
||||||
|
<Trans>Mark Unwatched</Trans>
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ScaledIcon icon={IconCheck} size={16} color={titleAccentForeground} />
|
<ScaledIcon icon={IconCheck} size={16} color={titleAccentForeground} />
|
||||||
|
|||||||
@@ -126,7 +126,14 @@ export function UpcomingRow({ item }: { item: UpcomingItem }) {
|
|||||||
<Link.Trigger withAppleZoom>{rowContent}</Link.Trigger>
|
<Link.Trigger withAppleZoom>{rowContent}</Link.Trigger>
|
||||||
<Link.Preview />
|
<Link.Preview />
|
||||||
<Link.Menu>
|
<Link.Menu>
|
||||||
{item.titleType === "movie" && (
|
{item.titleType === "movie" && item.userStatus === "completed" && (
|
||||||
|
<Link.MenuAction
|
||||||
|
title={t`Mark Unwatched`}
|
||||||
|
icon="xmark.circle"
|
||||||
|
onPress={() => titleActions.unwatchMovie(item.titleId, item.titleName)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.titleType === "movie" && item.userStatus !== "completed" && (
|
||||||
<Link.MenuAction
|
<Link.MenuAction
|
||||||
title={t`Mark Watched`}
|
title={t`Mark Watched`}
|
||||||
icon="checkmark.circle"
|
icon="checkmark.circle"
|
||||||
|
|||||||
@@ -217,7 +217,14 @@ export const PosterCard = memo(function PosterCard({
|
|||||||
onPress={handleQuickAddPress}
|
onPress={handleQuickAddPress}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{type === "movie" && (
|
{type === "movie" && userStatus === "completed" && (
|
||||||
|
<Link.MenuAction
|
||||||
|
title={t`Mark Unwatched`}
|
||||||
|
icon="xmark.circle"
|
||||||
|
onPress={() => titleActions.unwatchMovie(id, title)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{type === "movie" && userStatus !== "completed" && (
|
||||||
<Link.MenuAction
|
<Link.MenuAction
|
||||||
title={t`Mark Watched`}
|
title={t`Mark Watched`}
|
||||||
icon="checkmark.circle"
|
icon="checkmark.circle"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface UseTitleActionsOptions {
|
|||||||
toasts?: {
|
toasts?: {
|
||||||
updateStatus?: ToastOverride<{ id: string; status: string | null }>;
|
updateStatus?: ToastOverride<{ id: string; status: string | null }>;
|
||||||
watchMovie?: ToastOverride<WatchInput>;
|
watchMovie?: ToastOverride<WatchInput>;
|
||||||
|
unwatchMovie?: ToastOverride<WatchInput>;
|
||||||
updateRating?: ToastOverride<{ id: string; stars: number }>;
|
updateRating?: ToastOverride<{ id: string; stars: number }>;
|
||||||
watchEpisode?: ToastOverride<WatchInput>;
|
watchEpisode?: ToastOverride<WatchInput>;
|
||||||
unwatchEpisode?: ToastOverride<WatchInput>;
|
unwatchEpisode?: ToastOverride<WatchInput>;
|
||||||
@@ -67,6 +68,16 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const unwatchMovie = useMutation(
|
||||||
|
orpc.tracking.unwatch.mutationOptions({
|
||||||
|
onSuccess: (_data, input) => {
|
||||||
|
toast.success(resolveToast(toastOverrides?.unwatchMovie, t`Marked as unwatched`, input));
|
||||||
|
invalidateTitleQueries();
|
||||||
|
},
|
||||||
|
onError: () => toast.error(t`Failed to mark as unwatched`),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const updateRating = useMutation(
|
const updateRating = useMutation(
|
||||||
orpc.tracking.rate.mutationOptions({
|
orpc.tracking.rate.mutationOptions({
|
||||||
onSuccess: (_data, input) => {
|
onSuccess: (_data, input) => {
|
||||||
@@ -115,6 +126,7 @@ export function useTitleActions(options?: UseTitleActionsOptions) {
|
|||||||
return {
|
return {
|
||||||
updateStatus,
|
updateStatus,
|
||||||
watchMovie,
|
watchMovie,
|
||||||
|
unwatchMovie,
|
||||||
updateRating,
|
updateRating,
|
||||||
watchEpisode,
|
watchEpisode,
|
||||||
unwatchEpisode,
|
unwatchEpisode,
|
||||||
|
|||||||
@@ -55,6 +55,20 @@ export const titleActions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async unwatchMovie(id: string, titleName?: string) {
|
||||||
|
try {
|
||||||
|
await client.tracking.unwatch({ scope: "movie", ids: [id] });
|
||||||
|
toast.success(
|
||||||
|
titleName
|
||||||
|
? i18n._(msg`Marked "${titleName}" as unwatched`)
|
||||||
|
: i18n._(msg`Marked as unwatched`),
|
||||||
|
);
|
||||||
|
invalidateTitleQueries();
|
||||||
|
} catch {
|
||||||
|
toast.error(i18n._(msg`Failed to mark as unwatched`));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async removeFromLibrary(id: string) {
|
async removeFromLibrary(id: string) {
|
||||||
try {
|
try {
|
||||||
await client.tracking.updateStatus({ id, status: null });
|
await client.tracking.updateStatus({ id, status: null });
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { IconCheck } from "@tabler/icons-react";
|
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
@@ -12,19 +12,31 @@ import { useTitleActions } from "./use-title-actions";
|
|||||||
export function TitleActions() {
|
export function TitleActions() {
|
||||||
const { titleType } = useTitleContext();
|
const { titleType } = useTitleContext();
|
||||||
const { userStatus, userRating } = useTitleUserInfo();
|
const { userStatus, userRating } = useTitleUserInfo();
|
||||||
const { handleStatusChange, handleRating, handleWatchMovie } = useTitleActions();
|
const { handleStatusChange, handleRating, handleWatchMovie, handleUnwatchMovie } =
|
||||||
|
useTitleActions();
|
||||||
|
const isMovieWatched = userStatus === "completed";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<StatusButton currentStatus={userStatus ?? null} onChange={handleStatusChange} />
|
<StatusButton currentStatus={userStatus ?? null} onChange={handleStatusChange} />
|
||||||
{titleType === "movie" && (
|
{titleType === "movie" && (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleWatchMovie}
|
onClick={isMovieWatched ? handleUnwatchMovie : handleWatchMovie}
|
||||||
|
variant={isMovieWatched ? "destructive" : "default"}
|
||||||
size="lg"
|
size="lg"
|
||||||
className="hover:shadow-primary/20 h-9 rounded-lg px-4 text-sm hover:shadow-md active:scale-[0.97]"
|
className={`h-9 rounded-lg px-4 text-sm hover:shadow-md active:scale-[0.97] ${isMovieWatched ? "hover:shadow-destructive/20" : "hover:shadow-primary/20"}`}
|
||||||
>
|
>
|
||||||
<IconCheck aria-hidden={true} className="size-3.5" />
|
{isMovieWatched ? (
|
||||||
<Trans>Mark Watched</Trans>
|
<>
|
||||||
|
<IconX aria-hidden={true} className="size-3.5" />
|
||||||
|
<Trans>Mark Unwatched</Trans>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<IconCheck aria-hidden={true} className="size-3.5" />
|
||||||
|
<Trans>Mark Watched</Trans>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Separator orientation="vertical" className="bg-border/50 mx-0.5 my-auto h-6" />
|
<Separator orientation="vertical" className="bg-border/50 mx-0.5 my-auto h-6" />
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { useTitleActions } from "./use-title-actions";
|
|||||||
export function TitleKeyboardShortcuts() {
|
export function TitleKeyboardShortcuts() {
|
||||||
const { titleType } = useTitleContext();
|
const { titleType } = useTitleContext();
|
||||||
const { userStatus } = useTitleUserInfo();
|
const { userStatus } = useTitleUserInfo();
|
||||||
const { handleStatusChange, handleRating, handleWatchMovie } = useTitleActions();
|
const { handleStatusChange, handleRating, handleWatchMovie, handleUnwatchMovie } =
|
||||||
|
useTitleActions();
|
||||||
|
|
||||||
const commandPaletteOpen = useAtomValue(commandPaletteOpenAtom);
|
const commandPaletteOpen = useAtomValue(commandPaletteOpenAtom);
|
||||||
const enabled = !commandPaletteOpen;
|
const enabled = !commandPaletteOpen;
|
||||||
@@ -21,7 +22,13 @@ export function TitleKeyboardShortcuts() {
|
|||||||
useHotkey(
|
useHotkey(
|
||||||
"M",
|
"M",
|
||||||
() => {
|
() => {
|
||||||
if (titleType === "movie") handleWatchMovie();
|
if (titleType === "movie") {
|
||||||
|
if (userStatus === "completed") {
|
||||||
|
handleUnwatchMovie();
|
||||||
|
} else {
|
||||||
|
handleWatchMovie();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ enabled },
|
{ enabled },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -135,6 +135,19 @@ export function useTitleActions() {
|
|||||||
}
|
}
|
||||||
}, [getUserInfo, setUserInfo, queryClient, userInfoKey, titleId, titleName, watch, t]);
|
}, [getUserInfo, setUserInfo, queryClient, userInfoKey, titleId, titleName, watch, t]);
|
||||||
|
|
||||||
|
const handleUnwatchMovie = useCallback(async () => {
|
||||||
|
await queryClient.cancelQueries({ queryKey: userInfoKey });
|
||||||
|
const prevStatus = getUserInfo().status;
|
||||||
|
setUserInfo((old) => ({ ...old, status: "in_watchlist" }));
|
||||||
|
try {
|
||||||
|
await unwatch({ scope: "movie", ids: [titleId] });
|
||||||
|
toast.success(t`Marked "${titleName}" as unwatched`);
|
||||||
|
} catch {
|
||||||
|
setUserInfo((old) => ({ ...old, status: prevStatus }));
|
||||||
|
toast.error(t`Failed to mark as unwatched`);
|
||||||
|
}
|
||||||
|
}, [getUserInfo, setUserInfo, queryClient, userInfoKey, titleId, titleName, unwatch, t]);
|
||||||
|
|
||||||
const handleWatchEpisode = useCallback(
|
const handleWatchEpisode = useCallback(
|
||||||
async (episodeId: string, seasonNum: number, epNum: number, isWatched: boolean) => {
|
async (episodeId: string, seasonNum: number, epNum: number, isWatched: boolean) => {
|
||||||
await queryClient.cancelQueries({ queryKey: userInfoKey });
|
await queryClient.cancelQueries({ queryKey: userInfoKey });
|
||||||
@@ -352,6 +365,7 @@ export function useTitleActions() {
|
|||||||
handleStatusChange,
|
handleStatusChange,
|
||||||
handleRating,
|
handleRating,
|
||||||
handleWatchMovie,
|
handleWatchMovie,
|
||||||
|
handleUnwatchMovie,
|
||||||
handleWatchEpisode,
|
handleWatchEpisode,
|
||||||
handleMarkSeason,
|
handleMarkSeason,
|
||||||
handleUnmarkSeason,
|
handleUnmarkSeason,
|
||||||
|
|||||||
@@ -346,7 +346,6 @@ msgstr ""
|
|||||||
msgid "Added \"{titleName}\" to watchlist"
|
msgid "Added \"{titleName}\" to watchlist"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/hooks/use-title-actions.ts
|
|
||||||
#: apps/native/src/hooks/use-title-actions.ts
|
#: apps/native/src/hooks/use-title-actions.ts
|
||||||
#: apps/native/src/lib/title-actions.ts
|
#: apps/native/src/lib/title-actions.ts
|
||||||
#: apps/web/src/components/titles/use-title-actions.ts
|
#: apps/web/src/components/titles/use-title-actions.ts
|
||||||
@@ -1236,7 +1235,6 @@ msgstr "Export"
|
|||||||
msgid "Failed"
|
msgid "Failed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/hooks/use-title-actions.ts
|
|
||||||
#: apps/native/src/lib/title-actions.ts
|
#: apps/native/src/lib/title-actions.ts
|
||||||
msgid "Failed to add to watchlist"
|
msgid "Failed to add to watchlist"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1293,6 +1291,12 @@ msgstr ""
|
|||||||
msgid "Failed to mark all episodes as watched"
|
msgid "Failed to mark all episodes as watched"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/hooks/use-title-actions.ts
|
||||||
|
#: apps/native/src/lib/title-actions.ts
|
||||||
|
#: apps/web/src/components/titles/use-title-actions.ts
|
||||||
|
msgid "Failed to mark as unwatched"
|
||||||
|
msgstr "Failed to mark as unwatched"
|
||||||
|
|
||||||
#: apps/native/src/hooks/use-title-actions.ts
|
#: apps/native/src/hooks/use-title-actions.ts
|
||||||
#: apps/native/src/lib/title-actions.ts
|
#: apps/native/src/lib/title-actions.ts
|
||||||
#: apps/web/src/components/titles/use-title-actions.ts
|
#: apps/web/src/components/titles/use-title-actions.ts
|
||||||
@@ -1386,7 +1390,6 @@ msgstr ""
|
|||||||
msgid "Failed to update rating"
|
msgid "Failed to update rating"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
|
||||||
#: apps/web/src/components/settings/registration-section.tsx
|
#: apps/web/src/components/settings/registration-section.tsx
|
||||||
msgid "Failed to update registration setting"
|
msgid "Failed to update registration setting"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1860,6 +1863,13 @@ msgstr "Mark episode {epNum} watched"
|
|||||||
msgid "Mark Episode Watched"
|
msgid "Mark Episode Watched"
|
||||||
msgstr "Mark Episode Watched"
|
msgstr "Mark Episode Watched"
|
||||||
|
|
||||||
|
#: apps/native/src/app/title/[id].tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
|
#: apps/web/src/components/titles/title-actions.tsx
|
||||||
|
msgid "Mark Unwatched"
|
||||||
|
msgstr "Mark Unwatched"
|
||||||
|
|
||||||
#: apps/web/src/components/command-palette.tsx
|
#: apps/web/src/components/command-palette.tsx
|
||||||
msgid "Mark watched"
|
msgid "Mark watched"
|
||||||
msgstr "Mark watched"
|
msgstr "Mark watched"
|
||||||
@@ -1871,6 +1881,10 @@ msgstr "Mark watched"
|
|||||||
msgid "Mark Watched"
|
msgid "Mark Watched"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/app/title/[id].tsx
|
||||||
|
msgid "Marked \"{name}\" as unwatched"
|
||||||
|
msgstr "Marked \"{name}\" as unwatched"
|
||||||
|
|
||||||
#: apps/native/src/app/title/[id].tsx
|
#: apps/native/src/app/title/[id].tsx
|
||||||
msgid "Marked \"{name}\" as watched"
|
msgid "Marked \"{name}\" as watched"
|
||||||
msgstr "Marked \"{name}\" as watched"
|
msgstr "Marked \"{name}\" as watched"
|
||||||
@@ -1879,6 +1893,11 @@ msgstr "Marked \"{name}\" as watched"
|
|||||||
#~ msgid "Marked \"{titleName}\" as completed"
|
#~ msgid "Marked \"{titleName}\" as completed"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/lib/title-actions.ts
|
||||||
|
#: apps/web/src/components/titles/use-title-actions.ts
|
||||||
|
msgid "Marked \"{titleName}\" as unwatched"
|
||||||
|
msgstr "Marked \"{titleName}\" as unwatched"
|
||||||
|
|
||||||
#: apps/native/src/lib/title-actions.ts
|
#: apps/native/src/lib/title-actions.ts
|
||||||
#: apps/web/src/components/titles/use-title-actions.ts
|
#: apps/web/src/components/titles/use-title-actions.ts
|
||||||
msgid "Marked \"{titleName}\" as watched"
|
msgid "Marked \"{titleName}\" as watched"
|
||||||
@@ -1898,6 +1917,12 @@ msgstr "Marked all episodes of \"{titleName}\" as watched"
|
|||||||
#~ msgid "Marked as completed"
|
#~ msgid "Marked as completed"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/app/title/[id].tsx
|
||||||
|
#: apps/native/src/hooks/use-title-actions.ts
|
||||||
|
#: apps/native/src/lib/title-actions.ts
|
||||||
|
msgid "Marked as unwatched"
|
||||||
|
msgstr "Marked as unwatched"
|
||||||
|
|
||||||
#: apps/native/src/app/title/[id].tsx
|
#: apps/native/src/app/title/[id].tsx
|
||||||
#: apps/native/src/hooks/use-title-actions.ts
|
#: apps/native/src/hooks/use-title-actions.ts
|
||||||
#: apps/native/src/lib/title-actions.ts
|
#: apps/native/src/lib/title-actions.ts
|
||||||
|
|||||||
Reference in New Issue
Block a user