mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-15 22:55:42 -04:00
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:
21 files changed
+432
-6
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,
|
||||
|
||||
@@ -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,16 +30,25 @@ 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 (
|
||||
<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 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"
|
||||
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"
|
||||
>
|
||||
<div
|
||||
className="bg-muted relative aspect-video overflow-hidden rounded-t-xl"
|
||||
@@ -86,9 +100,37 @@ export function ContinueWatchingCard({ item }: { item: ContinueWatchingItemProps
|
||||
</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
|
||||
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>
|
||||
)}
|
||||
</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,10 +66,11 @@ export function UpcomingRow({ item }: { item: UpcomingItem }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<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 px-3 py-3 ring-1 ring-white/[0.06] transition-[background,box-shadow,ring-color] duration-200 hover:shadow-lg"
|
||||
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]">
|
||||
@@ -129,5 +145,27 @@ export function UpcomingRow({ item }: { item: UpcomingItem }) {
|
||||
)}
|
||||
</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 };
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user