feat(dashboard): mark episodes watched from Continue Watching and Upcoming

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Sonnet 5.5 committed 2026-10-05 14:20:09 -04:00
1 parent 899be96316
commit 04bcb66436
21 files changed
+557 -131

No files matched your search

@@ -21,6 +21,7 @@ export interface ContinueWatchingItem {
watchedEpisodes: number;
totalEpisodes: number;
nextEpisode?: {
id: string;
seasonNumber: number;
episodeNumber: number;
name: string | null;
@@ -43,6 +44,7 @@ export const ContinueWatchingCard = memo(function ContinueWatchingCard({
const seasonNum = item.nextEpisode?.seasonNumber;
const epNum = item.nextEpisode?.episodeNumber;
const nextEpLabel = item.nextEpisode ? t`Next: S${seasonNum} E${epNum}` : undefined;
const nextEpisodeId = item.nextEpisode?.id;
const cardLabel = [item.title.title, progressLabel, nextEpLabel].filter(Boolean).join(", ");
return (
@@ -107,6 +109,13 @@ export const ContinueWatchingCard = memo(function ContinueWatchingCard({
</Link.Trigger>
<Link.Preview />
<Link.Menu>
{nextEpisodeId && (
<Link.MenuAction
title={t`Mark S${seasonNum} E${epNum} Watched`}
icon="checkmark.circle"
onPress={() => titleActions.watchEpisode(nextEpisodeId)}
/>
)}
<Link.MenuAction
title={t`Remove from library`}
icon="trash"
@@ -10,6 +10,7 @@ import { ScaledIcon } from "@/components/ui/scaled-icon";
import { Text } from "@/components/ui/text";
import { titleActions } from "@/lib/title-actions";
import type { UpcomingItem } from "@sofa/api/schemas";
import { formatLocalDate } from "@sofa/i18n/date-buckets";
const statusColors = {
in_watchlist: "--color-status-watchlist",
@@ -27,6 +28,10 @@ function formatShortDate(dateStr: string): string {
}).format(d);
}
function hasAired(date: string): boolean {
return date <= formatLocalDate(new Date());
}
export function UpcomingRow({ item }: { item: UpcomingItem }) {
const { t } = useLingui();
const statusColor = useCSSVariable(statusColors[item.userStatus]) as string;
@@ -140,7 +145,7 @@ export function UpcomingRow({ item }: { item: UpcomingItem }) {
onPress={() => titleActions.markMovieWatched(item.titleId, item.titleName)}
/>
)}
{item.titleType === "tv" && item.episodeId && (
{item.titleType === "tv" && item.episodeId && hasAired(item.date) && (
<Link.MenuAction
title={t`Mark Episode Watched`}
icon="checkmark.circle"
@@ -63,6 +63,7 @@ export const continueWatching = os.library.continueWatching.use(authed).handler(
},
nextEpisode: item.nextEpisode
? {
id: item.nextEpisode.id,
seasonNumber: item.nextEpisode.seasonNumber,
episodeNumber: item.nextEpisode.episodeNumber,
name: item.nextEpisode.name,
+52
View File
@@ -0,0 +1,52 @@
import { call } from "@orpc/server";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { auth } from "@sofa/auth/server";
import {
clearAllTables,
insertEpisodeWatch,
insertStatus,
insertTvShow,
insertUser,
} from "@sofa/test/db";
import { implementedRouter } from "../src/orpc/router";
vi.mock("@sofa/auth/server", () => ({
auth: {
api: { getSession: vi.fn<() => void>() },
handler: vi.fn<() => void>(),
},
}));
const getSession = vi.mocked(auth.api.getSession);
const ctx = { context: { headers: new Headers() } };
function sessionFor(userId: string) {
return {
session: { id: `session-${userId}`, userId },
user: { id: userId, role: "user", name: "U", email: `${userId}@example.com` },
} as never;
}
beforeEach(() => {
clearAllTables();
insertUser("user-1");
getSession.mockReset();
getSession.mockResolvedValue(sessionFor("user-1"));
});
describe("library.continueWatching", () => {
test("includes the next episode id", async () => {
const { titleId, episodeIds } = insertTvShow("tv-1", 1001, 1, 3, {
airDates: ["2020-01-01", "2020-01-08", "2020-01-15"],
});
insertStatus("user-1", titleId, "in_progress");
insertEpisodeWatch("user-1", episodeIds[0]!);
const result = await call(implementedRouter.library.continueWatching, undefined, ctx);
expect(result.items).toHaveLength(1);
expect(result.items[0]?.nextEpisode?.id).toBe(episodeIds[1]);
});
});
@@ -1,10 +1,14 @@
import { plural } from "@lingui/core/macro";
import { useLingui } from "@lingui/react/macro";
import { IconPlayerPlay } from "@tabler/icons-react";
import { IconCheck, IconPlayerPlay } from "@tabler/icons-react";
import { Link } from "@tanstack/react-router";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { thumbHashToUrl } from "@/lib/thumbhash";
import { useQuickWatchEpisode } from "./use-quick-watch";
export interface ContinueWatchingItemProps {
title: {
id: string;
@@ -13,6 +17,7 @@ export interface ContinueWatchingItemProps {
backdropThumbHash?: string | null;
};
nextEpisode: {
id: string;
seasonNumber: number;
episodeNumber: number;
name: string | null;
@@ -25,70 +30,107 @@ export interface ContinueWatchingItemProps {
export function ContinueWatchingCard({ item }: { item: ContinueWatchingItemProps }) {
const { t } = useLingui();
const { watchEpisode, pendingId } = useQuickWatchEpisode();
const stillUrl = item.nextEpisode?.stillPath ?? item.title.backdropPath ?? null;
const progress = item.totalEpisodes > 0 ? (item.watchedEpisodes / item.totalEpisodes) * 100 : 0;
const watchedEpisodes = item.watchedEpisodes;
const totalEpisodes = item.totalEpisodes;
const nextEpisode = item.nextEpisode;
const quickWatchSeason = nextEpisode?.seasonNumber;
const quickWatchEpisode = nextEpisode?.episodeNumber;
const quickWatchLabel = nextEpisode
? t`Mark S${quickWatchSeason} E${quickWatchEpisode} watched`
: null;
return (
<Link
to="/titles/$id"
params={{ id: item.title.id }}
className="group bg-card/50 relative inline-block w-64 shrink-0 overflow-hidden rounded-xl ring-1 ring-white/[0.06] transition-shadow hover:shadow-lg hover:shadow-black/25 sm:w-72"
>
<div
className="bg-muted relative aspect-video overflow-hidden rounded-t-xl"
style={(() => {
const hash = item.nextEpisode?.stillThumbHash ?? item.title.backdropThumbHash;
const url = thumbHashToUrl(hash);
return url ? { backgroundImage: `url(${url})`, backgroundSize: "cover" } : undefined;
})()}
<div className="relative w-64 shrink-0 sm:w-72">
<Link
to="/titles/$id"
params={{ id: item.title.id }}
className="group bg-card/50 relative block w-full overflow-hidden rounded-xl ring-1 ring-white/[0.06] transition-shadow hover:shadow-lg hover:shadow-black/25"
>
{stillUrl ? (
<img
src={stillUrl}
alt={item.nextEpisode?.name ?? item.title.title}
loading="lazy"
decoding="async"
className="absolute inset-0 h-full w-full object-cover motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-105"
/>
) : (
<div className="from-card via-secondary to-muted flex h-full items-center justify-center bg-gradient-to-br">
<IconPlayerPlay aria-hidden={true} className="text-muted-foreground/30 size-8" />
</div>
)}
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent" />
{item.nextEpisode && (
<div className="absolute right-3 bottom-2.5 left-3">
<p className="text-primary flex items-center gap-1.5 text-[10px] font-medium tracking-wider uppercase">
<span className="bg-primary inline-block h-1.5 w-1.5 rounded-full motion-safe:animate-pulse" />
{t`Up next`}
</p>
<p className="mt-0.5 truncate text-sm font-medium text-white">
<span className="me-0.5 font-mono text-xs text-white/60 [word-spacing:-0.25em]">
S{item.nextEpisode.seasonNumber} E{item.nextEpisode.episodeNumber}
</span>{" "}
{item.nextEpisode.name}
<div
className="bg-muted relative aspect-video overflow-hidden rounded-t-xl"
style={(() => {
const hash = item.nextEpisode?.stillThumbHash ?? item.title.backdropThumbHash;
const url = thumbHashToUrl(hash);
return url ? { backgroundImage: `url(${url})`, backgroundSize: "cover" } : undefined;
})()}
>
{stillUrl ? (
<img
src={stillUrl}
alt={item.nextEpisode?.name ?? item.title.title}
loading="lazy"
decoding="async"
className="absolute inset-0 h-full w-full object-cover motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-105"
/>
) : (
<div className="from-card via-secondary to-muted flex h-full items-center justify-center bg-gradient-to-br">
<IconPlayerPlay aria-hidden={true} className="text-muted-foreground/30 size-8" />
</div>
)}
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent" />
{item.nextEpisode && (
<div className="absolute right-3 bottom-2.5 left-3">
<p className="text-primary flex items-center gap-1.5 text-[10px] font-medium tracking-wider uppercase">
<span className="bg-primary inline-block h-1.5 w-1.5 rounded-full motion-safe:animate-pulse" />
{t`Up next`}
</p>
<p className="mt-0.5 truncate text-sm font-medium text-white">
<span className="me-0.5 font-mono text-xs text-white/60 [word-spacing:-0.25em]">
S{item.nextEpisode.seasonNumber} E{item.nextEpisode.episodeNumber}
</span>{" "}
{item.nextEpisode.name}
</p>
</div>
)}
</div>
<div className="flex items-center gap-3 p-3">
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{item.title.title}</p>
<p className="text-muted-foreground text-xs">
{t`${watchedEpisodes}/${plural(totalEpisodes, { one: "# episode", other: "# episodes" })}`}
</p>
</div>
<div className="bg-primary/10 text-primary group-hover:bg-primary group-hover:text-primary-foreground flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-colors">
<IconPlayerPlay aria-hidden={true} className="size-3.5" />
</div>
</div>
{progress > 0 && (
<div className="bg-muted absolute right-0 bottom-0 left-0 h-0.5">
<div
className="bg-primary h-full transition-[width]"
style={{ width: `${progress}%` }}
/>
</div>
)}
</div>
<div className="flex items-center gap-3 p-3">
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{item.title.title}</p>
<p className="text-muted-foreground text-xs">
{t`${watchedEpisodes}/${plural(totalEpisodes, { one: "# episode", other: "# episodes" })}`}
</p>
</div>
<div className="bg-primary/10 text-primary group-hover:bg-primary group-hover:text-primary-foreground flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-colors">
<IconPlayerPlay aria-hidden={true} className="size-3.5" />
</div>
</div>
{progress > 0 && (
<div className="bg-muted absolute right-0 bottom-0 left-0 h-0.5">
<div className="bg-primary h-full transition-[width]" style={{ width: `${progress}%` }} />
</div>
</Link>
{nextEpisode && quickWatchLabel && (
<Tooltip>
<TooltipTrigger
render={
<Button
variant="secondary"
size="icon"
aria-label={quickWatchLabel}
disabled={pendingId === nextEpisode.id}
className="absolute top-2 right-2 rounded-full bg-black/50 text-white backdrop-blur-sm hover:bg-black/70"
onClick={() =>
watchEpisode(
nextEpisode.id,
`S${nextEpisode.seasonNumber} E${nextEpisode.episodeNumber}`,
)
}
/>
}
>
<IconCheck aria-hidden={true} />
</TooltipTrigger>
<TooltipContent side="bottom">{quickWatchLabel}</TooltipContent>
</Tooltip>
)}
</Link>
</div>
);
}
@@ -1,11 +1,15 @@
import { plural } from "@lingui/core/macro";
import { useLingui } from "@lingui/react/macro";
import { IconMovie } from "@tabler/icons-react";
import { IconCheck, IconMovie } from "@tabler/icons-react";
import { Link } from "@tanstack/react-router";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { thumbHashToUrl } from "@/lib/thumbhash";
import type { UpcomingItem } from "@sofa/api/schemas";
import { formatLocalDate } from "@sofa/i18n/date-buckets";
import { useQuickWatchEpisode } from "./use-quick-watch";
const statusColorClass = {
in_watchlist: "bg-status-watchlist",
@@ -25,8 +29,19 @@ function formatShortDate(dateStr: string): string {
}).format(d);
}
function hasAired(date: string): boolean {
return date <= formatLocalDate(new Date());
}
export function UpcomingRow({ item }: { item: UpcomingItem }) {
const { t } = useLingui();
const { watchEpisode, pendingId } = useQuickWatchEpisode();
const quickWatchEpisodeId =
item.titleType === "tv" && item.episodeId && hasAired(item.date) ? item.episodeId : null;
const quickWatchSeason = item.seasonNumber ?? 0;
const quickWatchEpisode = item.episodeNumber ?? 0;
const quickWatchLabel = t`Mark S${quickWatchSeason} E${quickWatchEpisode} watched`;
const statusLabels = {
in_watchlist: t`On Watchlist`,
@@ -51,83 +66,106 @@ export function UpcomingRow({ item }: { item: UpcomingItem }) {
}
return (
<Link
to="/titles/$id"
params={{ id: item.titleId }}
className="group bg-card/40 hover:bg-card/60 hover:shadow-primary/5 hover:ring-primary/25 flex items-center gap-4 rounded-xl px-3 py-3 ring-1 ring-white/[0.06] transition-[background,box-shadow,ring-color] duration-200 hover:shadow-lg"
>
{/* Poster */}
<div className="relative h-[66px] w-11 shrink-0 overflow-hidden rounded-lg ring-1 ring-white/[0.06]">
{item.posterPath ? (
<img
src={item.posterPath}
alt=""
className="size-full object-cover motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-105"
loading="lazy"
{...(item.posterThumbHash
? {
style: {
background: `url(${thumbHashToUrl(item.posterThumbHash)}) center/cover`,
},
}
: {})}
/>
) : (
<div className="bg-muted flex size-full items-center justify-center">
<IconMovie className="text-muted-foreground size-5" />
</div>
)}
</div>
{/* Content */}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger className="relative flex size-2 shrink-0">
<span
className={`absolute inline-flex size-full rounded-full opacity-40 motion-safe:animate-pulse ${statusHaloClass[item.userStatus]}`}
/>
<span
className={`relative inline-flex size-2 rounded-full ${statusColorClass[item.userStatus]}`}
/>
</TooltipTrigger>
<TooltipContent side="top">{statusLabels[item.userStatus]}</TooltipContent>
</Tooltip>
<span className="truncate text-sm font-medium">{item.titleName}</span>
{item.isNewSeason && (
<span className="bg-primary/15 text-primary shrink-0 rounded px-1.5 py-0.5 text-[10px] font-semibold tracking-wider uppercase">
{t`New Season`}
</span>
<div className="relative">
<Link
to="/titles/$id"
params={{ id: item.titleId }}
className={`group bg-card/40 hover:bg-card/60 hover:shadow-primary/5 hover:ring-primary/25 flex items-center gap-4 rounded-xl py-3 ps-3 ring-1 ring-white/[0.06] transition-[background,box-shadow,ring-color] duration-200 hover:shadow-lg ${quickWatchEpisodeId ? "pe-12" : "pe-3"}`}
>
{/* Poster */}
<div className="relative h-[66px] w-11 shrink-0 overflow-hidden rounded-lg ring-1 ring-white/[0.06]">
{item.posterPath ? (
<img
src={item.posterPath}
alt=""
className="size-full object-cover motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-105"
loading="lazy"
{...(item.posterThumbHash
? {
style: {
background: `url(${thumbHashToUrl(item.posterThumbHash)}) center/cover`,
},
}
: {})}
/>
) : (
<div className="bg-muted flex size-full items-center justify-center">
<IconMovie className="text-muted-foreground size-5" />
</div>
)}
</div>
<div className="text-muted-foreground mt-1 flex items-center gap-1.5 text-xs">
{item.titleType === "movie" && <IconMovie className="size-3 shrink-0" />}
<span className="truncate">{subtitle}</span>
</div>
</div>
{/* Right column: date + provider logo */}
<div className="flex shrink-0 flex-col items-end gap-1">
<span className="text-muted-foreground text-xs">{formatShortDate(item.date)}</span>
{item.streamingProvider && (
<Tooltip>
<TooltipTrigger className="shrink-0">
{item.streamingProvider.logoPath ? (
<img
src={item.streamingProvider.logoPath}
alt={item.streamingProvider.providerName}
className="size-7 rounded-lg ring-1 ring-white/[0.06]"
{/* Content */}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger className="relative flex size-2 shrink-0">
<span
className={`absolute inline-flex size-full rounded-full opacity-40 motion-safe:animate-pulse ${statusHaloClass[item.userStatus]}`}
/>
) : (
<span className="text-muted-foreground/60 text-[10px]">
{item.streamingProvider.providerName}
</span>
)}
</TooltipTrigger>
<TooltipContent side="top">{item.streamingProvider.providerName}</TooltipContent>
</Tooltip>
)}
</div>
</Link>
<span
className={`relative inline-flex size-2 rounded-full ${statusColorClass[item.userStatus]}`}
/>
</TooltipTrigger>
<TooltipContent side="top">{statusLabels[item.userStatus]}</TooltipContent>
</Tooltip>
<span className="truncate text-sm font-medium">{item.titleName}</span>
{item.isNewSeason && (
<span className="bg-primary/15 text-primary shrink-0 rounded px-1.5 py-0.5 text-[10px] font-semibold tracking-wider uppercase">
{t`New Season`}
</span>
)}
</div>
<div className="text-muted-foreground mt-1 flex items-center gap-1.5 text-xs">
{item.titleType === "movie" && <IconMovie className="size-3 shrink-0" />}
<span className="truncate">{subtitle}</span>
</div>
</div>
{/* Right column: date + provider logo */}
<div className="flex shrink-0 flex-col items-end gap-1">
<span className="text-muted-foreground text-xs">{formatShortDate(item.date)}</span>
{item.streamingProvider && (
<Tooltip>
<TooltipTrigger className="shrink-0">
{item.streamingProvider.logoPath ? (
<img
src={item.streamingProvider.logoPath}
alt={item.streamingProvider.providerName}
className="size-7 rounded-lg ring-1 ring-white/[0.06]"
/>
) : (
<span className="text-muted-foreground/60 text-[10px]">
{item.streamingProvider.providerName}
</span>
)}
</TooltipTrigger>
<TooltipContent side="top">{item.streamingProvider.providerName}</TooltipContent>
</Tooltip>
)}
</div>
</Link>
{quickWatchEpisodeId && (
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
size="icon"
aria-label={quickWatchLabel}
disabled={pendingId === quickWatchEpisodeId}
className="absolute end-3 top-1/2 -translate-y-1/2 rounded-full"
onClick={() =>
watchEpisode(quickWatchEpisodeId, `S${quickWatchSeason} E${quickWatchEpisode}`)
}
/>
}
>
<IconCheck aria-hidden={true} />
</TooltipTrigger>
<TooltipContent side="top">{quickWatchLabel}</TooltipContent>
</Tooltip>
)}
</div>
);
}
@@ -0,0 +1,33 @@
import { useLingui } from "@lingui/react/macro";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { getErrorMessage } from "@/lib/error-messages";
import { orpc } from "@/lib/orpc/client";
import { invalidateTrackingQueries } from "@/lib/orpc/invalidate";
/** Mark a single episode watched from a dashboard card, with toast + cache refresh. */
export function useQuickWatchEpisode() {
const { t } = useLingui();
const queryClient = useQueryClient();
const { mutate, variables, isPending } = useMutation(orpc.tracking.watch.mutationOptions());
function watchEpisode(episodeId: string, label: string) {
mutate(
{ scope: "episode", ids: [episodeId] },
{
onSuccess: () => {
toast.success(t`Marked ${label} as watched`);
void invalidateTrackingQueries(queryClient);
},
onError: (err) => {
toast.error(getErrorMessage(err, t`Failed to mark episode`));
},
},
);
}
const pendingId = isPending ? (variables?.ids?.[0] ?? null) : null;
return { watchEpisode, pendingId };
}
+5
View File
@@ -2816,6 +2816,10 @@
{
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Episode ID"
},
"seasonNumber": {
"type": "number",
"description": "Season number"
@@ -2859,6 +2863,7 @@
}
},
"required": [
"id",
"seasonNumber",
"episodeNumber",
"name",
+1
View File
@@ -560,6 +560,7 @@ export const ContinueWatchingOutput = z
}),
nextEpisode: z
.object({
id: z.string().describe("Episode ID"),
seasonNumber: z.number().describe("Season number"),
episodeNumber: z.number().describe("Episode number"),
name: z.string().nullable().describe("Episode title"),
+20
View File
@@ -1308,6 +1308,7 @@ msgstr "فشل التحديد كمشاهدة"
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/dashboard/use-quick-watch.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark episode"
msgstr "فشل تحديد الحلقة"
@@ -1867,6 +1868,21 @@ msgstr "تمييز الحلقة {epNum} كمشاهَدة"
msgid "Mark Episode Watched"
msgstr "تمييز الحلقة كمشاهَدة"
#. placeholder {0}: nextEpisode.seasonNumber
#. placeholder {1}: nextEpisode.episodeNumber
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#~ msgid "Mark S{0} E{1} watched"
#~ msgstr ""
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#: apps/web/src/components/dashboard/upcoming-item.tsx
msgid "Mark S{quickWatchSeason} E{quickWatchEpisode} watched"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
msgid "Mark S{seasonNum} E{epNum} Watched"
msgstr ""
#: apps/native/src/app/title/[id].tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/ui/poster-card.tsx
@@ -1907,6 +1923,10 @@ msgstr ""
msgid "Marked \"{titleName}\" as watched"
msgstr "تم تحديد \"{titleName}\" كمشاهدة"
#: apps/web/src/components/dashboard/use-quick-watch.ts
msgid "Marked {label} as watched"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Marked all episodes as watched"
+20
View File
@@ -1308,6 +1308,7 @@ msgstr "Konnte nicht als gesehen markiert werden"
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/dashboard/use-quick-watch.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark episode"
msgstr "Episode konnte nicht markiert werden"
@@ -1867,6 +1868,21 @@ msgstr "Episode {epNum} als gesehen markieren"
msgid "Mark Episode Watched"
msgstr "Episode als gesehen markieren"
#. placeholder {0}: nextEpisode.seasonNumber
#. placeholder {1}: nextEpisode.episodeNumber
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#~ msgid "Mark S{0} E{1} watched"
#~ msgstr ""
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#: apps/web/src/components/dashboard/upcoming-item.tsx
msgid "Mark S{quickWatchSeason} E{quickWatchEpisode} watched"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
msgid "Mark S{seasonNum} E{epNum} Watched"
msgstr ""
#: apps/native/src/app/title/[id].tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/ui/poster-card.tsx
@@ -1907,6 +1923,10 @@ msgstr ""
msgid "Marked \"{titleName}\" as watched"
msgstr "\"{titleName}\" als gesehen markiert"
#: apps/web/src/components/dashboard/use-quick-watch.ts
msgid "Marked {label} as watched"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Marked all episodes as watched"
+20
View File
@@ -1308,6 +1308,7 @@ msgstr ""
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/dashboard/use-quick-watch.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark episode"
msgstr ""
@@ -1867,6 +1868,21 @@ msgstr "Mark episode {epNum} watched"
msgid "Mark Episode Watched"
msgstr "Mark Episode Watched"
#. placeholder {0}: nextEpisode.seasonNumber
#. placeholder {1}: nextEpisode.episodeNumber
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#~ msgid "Mark S{0} E{1} watched"
#~ msgstr "Mark S{0} E{1} watched"
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#: apps/web/src/components/dashboard/upcoming-item.tsx
msgid "Mark S{quickWatchSeason} E{quickWatchEpisode} watched"
msgstr "Mark S{quickWatchSeason} E{quickWatchEpisode} watched"
#: apps/native/src/components/dashboard/continue-watching-card.tsx
msgid "Mark S{seasonNum} E{epNum} Watched"
msgstr "Mark S{seasonNum} E{epNum} Watched"
#: apps/native/src/app/title/[id].tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/ui/poster-card.tsx
@@ -1907,6 +1923,10 @@ msgstr "Marked \"{titleName}\" as unwatched"
msgid "Marked \"{titleName}\" as watched"
msgstr ""
#: apps/web/src/components/dashboard/use-quick-watch.ts
msgid "Marked {label} as watched"
msgstr "Marked {label} as watched"
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Marked all episodes as watched"
+20
View File
@@ -1308,6 +1308,7 @@ msgstr "Error al marcar como visto"
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/dashboard/use-quick-watch.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark episode"
msgstr "Error al marcar el episodio"
@@ -1867,6 +1868,21 @@ msgstr "Marcar episodio {epNum} como visto"
msgid "Mark Episode Watched"
msgstr "Marcar episodio como visto"
#. placeholder {0}: nextEpisode.seasonNumber
#. placeholder {1}: nextEpisode.episodeNumber
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#~ msgid "Mark S{0} E{1} watched"
#~ msgstr ""
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#: apps/web/src/components/dashboard/upcoming-item.tsx
msgid "Mark S{quickWatchSeason} E{quickWatchEpisode} watched"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
msgid "Mark S{seasonNum} E{epNum} Watched"
msgstr ""
#: apps/native/src/app/title/[id].tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/ui/poster-card.tsx
@@ -1907,6 +1923,10 @@ msgstr ""
msgid "Marked \"{titleName}\" as watched"
msgstr "\"{titleName}\" marcado como visto"
#: apps/web/src/components/dashboard/use-quick-watch.ts
msgid "Marked {label} as watched"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Marked all episodes as watched"
+20
View File
@@ -1308,6 +1308,7 @@ msgstr "Échec du marquage comme visionné"
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/dashboard/use-quick-watch.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark episode"
msgstr "Échec du marquage de l'épisode"
@@ -1867,6 +1868,21 @@ msgstr "Marquer l'épisode {epNum} comme regardé"
msgid "Mark Episode Watched"
msgstr "Marquer l'épisode comme regardé"
#. placeholder {0}: nextEpisode.seasonNumber
#. placeholder {1}: nextEpisode.episodeNumber
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#~ msgid "Mark S{0} E{1} watched"
#~ msgstr ""
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#: apps/web/src/components/dashboard/upcoming-item.tsx
msgid "Mark S{quickWatchSeason} E{quickWatchEpisode} watched"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
msgid "Mark S{seasonNum} E{epNum} Watched"
msgstr ""
#: apps/native/src/app/title/[id].tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/ui/poster-card.tsx
@@ -1907,6 +1923,10 @@ msgstr ""
msgid "Marked \"{titleName}\" as watched"
msgstr "« {titleName} » marqué comme visionné"
#: apps/web/src/components/dashboard/use-quick-watch.ts
msgid "Marked {label} as watched"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Marked all episodes as watched"
+20
View File
@@ -1308,6 +1308,7 @@ msgstr "סימון כנצפה נכשל"
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/dashboard/use-quick-watch.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark episode"
msgstr "סימון הפרק נכשל"
@@ -1867,6 +1868,21 @@ msgstr "סמן פרק {epNum} כנצפה"
msgid "Mark Episode Watched"
msgstr "סמן פרק כנצפה"
#. placeholder {0}: nextEpisode.seasonNumber
#. placeholder {1}: nextEpisode.episodeNumber
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#~ msgid "Mark S{0} E{1} watched"
#~ msgstr ""
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#: apps/web/src/components/dashboard/upcoming-item.tsx
msgid "Mark S{quickWatchSeason} E{quickWatchEpisode} watched"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
msgid "Mark S{seasonNum} E{epNum} Watched"
msgstr ""
#: apps/native/src/app/title/[id].tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/ui/poster-card.tsx
@@ -1907,6 +1923,10 @@ msgstr ""
msgid "Marked \"{titleName}\" as watched"
msgstr "\"{titleName}\" סומן כנצפה"
#: apps/web/src/components/dashboard/use-quick-watch.ts
msgid "Marked {label} as watched"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Marked all episodes as watched"
+20
View File
@@ -1308,6 +1308,7 @@ msgstr "Impossibile segnare come visto"
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/dashboard/use-quick-watch.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark episode"
msgstr "Impossibile segnare l'episodio"
@@ -1867,6 +1868,21 @@ msgstr "Segna episodio {epNum} come visto"
msgid "Mark Episode Watched"
msgstr "Segna episodio come visto"
#. placeholder {0}: nextEpisode.seasonNumber
#. placeholder {1}: nextEpisode.episodeNumber
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#~ msgid "Mark S{0} E{1} watched"
#~ msgstr ""
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#: apps/web/src/components/dashboard/upcoming-item.tsx
msgid "Mark S{quickWatchSeason} E{quickWatchEpisode} watched"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
msgid "Mark S{seasonNum} E{epNum} Watched"
msgstr ""
#: apps/native/src/app/title/[id].tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/ui/poster-card.tsx
@@ -1907,6 +1923,10 @@ msgstr ""
msgid "Marked \"{titleName}\" as watched"
msgstr "Segnato \"{titleName}\" come visto"
#: apps/web/src/components/dashboard/use-quick-watch.ts
msgid "Marked {label} as watched"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Marked all episodes as watched"
+20
View File
@@ -1308,6 +1308,7 @@ msgstr "視聴済みにするのに失敗しました"
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/dashboard/use-quick-watch.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark episode"
msgstr "エピソードのマークに失敗しました"
@@ -1867,6 +1868,21 @@ msgstr "エピソード{epNum}を視聴済みにする"
msgid "Mark Episode Watched"
msgstr "エピソードを視聴済みにする"
#. placeholder {0}: nextEpisode.seasonNumber
#. placeholder {1}: nextEpisode.episodeNumber
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#~ msgid "Mark S{0} E{1} watched"
#~ msgstr ""
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#: apps/web/src/components/dashboard/upcoming-item.tsx
msgid "Mark S{quickWatchSeason} E{quickWatchEpisode} watched"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
msgid "Mark S{seasonNum} E{epNum} Watched"
msgstr ""
#: apps/native/src/app/title/[id].tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/ui/poster-card.tsx
@@ -1907,6 +1923,10 @@ msgstr ""
msgid "Marked \"{titleName}\" as watched"
msgstr "「{titleName}」を視聴済みにしました"
#: apps/web/src/components/dashboard/use-quick-watch.ts
msgid "Marked {label} as watched"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Marked all episodes as watched"
+20
View File
@@ -1308,6 +1308,7 @@ msgstr "시청 처리 실패"
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/dashboard/use-quick-watch.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark episode"
msgstr "에피소드 처리 실패"
@@ -1867,6 +1868,21 @@ msgstr "에피소드 {epNum} 시청함으로 표시"
msgid "Mark Episode Watched"
msgstr "에피소드 시청함으로 표시"
#. placeholder {0}: nextEpisode.seasonNumber
#. placeholder {1}: nextEpisode.episodeNumber
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#~ msgid "Mark S{0} E{1} watched"
#~ msgstr ""
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#: apps/web/src/components/dashboard/upcoming-item.tsx
msgid "Mark S{quickWatchSeason} E{quickWatchEpisode} watched"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
msgid "Mark S{seasonNum} E{epNum} Watched"
msgstr ""
#: apps/native/src/app/title/[id].tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/ui/poster-card.tsx
@@ -1907,6 +1923,10 @@ msgstr ""
msgid "Marked \"{titleName}\" as watched"
msgstr "\"{titleName}\"을(를) 시청 완료로 표시했습니다"
#: apps/web/src/components/dashboard/use-quick-watch.ts
msgid "Marked {label} as watched"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Marked all episodes as watched"
+20
View File
@@ -1308,6 +1308,7 @@ msgstr "Markeren als bekeken mislukt"
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/dashboard/use-quick-watch.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark episode"
msgstr "Aflevering markeren mislukt"
@@ -1867,6 +1868,21 @@ msgstr "Aflevering {epNum} als bekeken markeren"
msgid "Mark Episode Watched"
msgstr "Aflevering als bekeken markeren"
#. placeholder {0}: nextEpisode.seasonNumber
#. placeholder {1}: nextEpisode.episodeNumber
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#~ msgid "Mark S{0} E{1} watched"
#~ msgstr ""
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#: apps/web/src/components/dashboard/upcoming-item.tsx
msgid "Mark S{quickWatchSeason} E{quickWatchEpisode} watched"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
msgid "Mark S{seasonNum} E{epNum} Watched"
msgstr ""
#: apps/native/src/app/title/[id].tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/ui/poster-card.tsx
@@ -1907,6 +1923,10 @@ msgstr ""
msgid "Marked \"{titleName}\" as watched"
msgstr "\"{titleName}\" gemarkeerd als bekeken"
#: apps/web/src/components/dashboard/use-quick-watch.ts
msgid "Marked {label} as watched"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Marked all episodes as watched"
+20
View File
@@ -1308,6 +1308,7 @@ msgstr "Falha ao marcar como assistido"
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/dashboard/use-quick-watch.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark episode"
msgstr "Falha ao marcar episódio"
@@ -1867,6 +1868,21 @@ msgstr "Marcar episódio {epNum} como assistido"
msgid "Mark Episode Watched"
msgstr "Marcar Episódio como Assistido"
#. placeholder {0}: nextEpisode.seasonNumber
#. placeholder {1}: nextEpisode.episodeNumber
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#~ msgid "Mark S{0} E{1} watched"
#~ msgstr ""
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#: apps/web/src/components/dashboard/upcoming-item.tsx
msgid "Mark S{quickWatchSeason} E{quickWatchEpisode} watched"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
msgid "Mark S{seasonNum} E{epNum} Watched"
msgstr ""
#: apps/native/src/app/title/[id].tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/ui/poster-card.tsx
@@ -1907,6 +1923,10 @@ msgstr ""
msgid "Marked \"{titleName}\" as watched"
msgstr "Marcado \"{titleName}\" como assistido"
#: apps/web/src/components/dashboard/use-quick-watch.ts
msgid "Marked {label} as watched"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Marked all episodes as watched"
+20
View File
@@ -1308,6 +1308,7 @@ msgstr "标记为已观看失败"
#: apps/native/src/hooks/use-title-actions.ts
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/dashboard/use-quick-watch.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Failed to mark episode"
msgstr "标记单集失败"
@@ -1867,6 +1868,21 @@ msgstr "将第 {epNum} 集标记为已观看"
msgid "Mark Episode Watched"
msgstr "标记剧集为已观看"
#. placeholder {0}: nextEpisode.seasonNumber
#. placeholder {1}: nextEpisode.episodeNumber
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#~ msgid "Mark S{0} E{1} watched"
#~ msgstr ""
#: apps/web/src/components/dashboard/continue-watching-card.tsx
#: apps/web/src/components/dashboard/upcoming-item.tsx
msgid "Mark S{quickWatchSeason} E{quickWatchEpisode} watched"
msgstr ""
#: apps/native/src/components/dashboard/continue-watching-card.tsx
msgid "Mark S{seasonNum} E{epNum} Watched"
msgstr ""
#: apps/native/src/app/title/[id].tsx
#: apps/native/src/components/dashboard/upcoming-row.tsx
#: apps/native/src/components/ui/poster-card.tsx
@@ -1907,6 +1923,10 @@ msgstr ""
msgid "Marked \"{titleName}\" as watched"
msgstr "已将\"{titleName}\"标记为已观看"
#: apps/web/src/components/dashboard/use-quick-watch.ts
msgid "Marked {label} as watched"
msgstr ""
#: apps/native/src/lib/title-actions.ts
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Marked all episodes as watched"