mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-15 19:45:42 -04:00
feat(tracking): remove a single watch and log a watch on a date
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
30 files changed
+1715
-94
No files matched your search
@@ -8,7 +8,11 @@ import type { z } from "zod";
|
||||
import { Image } from "@/components/ui/image";
|
||||
import { ScaledIcon } from "@/components/ui/scaled-icon";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { getErrorMessage } from "@/lib/error-messages";
|
||||
import { formatLocalTimeOfDay } from "@/lib/format-time";
|
||||
import { client } from "@/lib/orpc";
|
||||
import { invalidateTitleQueries } from "@/lib/title-actions";
|
||||
import { toast } from "@/lib/toast";
|
||||
import type { WatchHistoryItemSchema } from "@sofa/api/schemas";
|
||||
|
||||
type WatchHistoryItem = z.infer<typeof WatchHistoryItemSchema>;
|
||||
@@ -36,46 +40,66 @@ export function HistoryRow({ item }: { item: WatchHistoryItem }) {
|
||||
const time = formatLocalTimeOfDay(new Date(item.watchedAt));
|
||||
const accessibilityLabel = [item.title.title, subtitle, time].filter(Boolean).join(", ");
|
||||
|
||||
async function removeFromHistory() {
|
||||
try {
|
||||
await client.tracking.deleteWatch({ kind: item.kind, watchId: item.watchId });
|
||||
toast.success(t`Removed from history`);
|
||||
await invalidateTitleQueries();
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err, t`Failed to remove from history`));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={`/title/${item.title.id}`} asChild>
|
||||
<Pressable
|
||||
accessibilityRole="link"
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
className="bg-card/40 flex-row items-center gap-3 rounded-xl border border-white/[0.06] px-3 py-3"
|
||||
>
|
||||
<View className="overflow-hidden rounded-lg" style={{ width: 44, height: 66 }}>
|
||||
{item.title.posterPath ? (
|
||||
<Image
|
||||
source={{ uri: item.title.posterPath }}
|
||||
thumbHash={item.title.posterThumbHash}
|
||||
className="size-full"
|
||||
contentFit="cover"
|
||||
/>
|
||||
) : (
|
||||
<View className="bg-muted size-full items-center justify-center">
|
||||
<ScaledIcon icon={IconMovie} size={18} color={mutedColor} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View className="min-w-0 flex-1">
|
||||
<Text className="text-foreground text-sm font-medium" numberOfLines={1}>
|
||||
{item.title.title}
|
||||
</Text>
|
||||
{subtitle && (
|
||||
<Text className="text-muted-foreground mt-1 text-xs" numberOfLines={1}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className="items-end gap-1">
|
||||
<Text className="text-muted-foreground text-xs">{time}</Text>
|
||||
<View className="bg-muted rounded px-1.5 py-0.5">
|
||||
<Text className="text-muted-foreground text-[10px] font-medium tracking-wider uppercase">
|
||||
{sourceLabels[item.source]}
|
||||
</Text>
|
||||
<Link.Trigger>
|
||||
<Pressable
|
||||
accessibilityRole="link"
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
className="bg-card/40 flex-row items-center gap-3 rounded-xl border border-white/[0.06] px-3 py-3"
|
||||
>
|
||||
<View className="overflow-hidden rounded-lg" style={{ width: 44, height: 66 }}>
|
||||
{item.title.posterPath ? (
|
||||
<Image
|
||||
source={{ uri: item.title.posterPath }}
|
||||
thumbHash={item.title.posterThumbHash}
|
||||
className="size-full"
|
||||
contentFit="cover"
|
||||
/>
|
||||
) : (
|
||||
<View className="bg-muted size-full items-center justify-center">
|
||||
<ScaledIcon icon={IconMovie} size={18} color={mutedColor} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
<View className="min-w-0 flex-1">
|
||||
<Text className="text-foreground text-sm font-medium" numberOfLines={1}>
|
||||
{item.title.title}
|
||||
</Text>
|
||||
{subtitle && (
|
||||
<Text className="text-muted-foreground mt-1 text-xs" numberOfLines={1}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className="items-end gap-1">
|
||||
<Text className="text-muted-foreground text-xs">{time}</Text>
|
||||
<View className="bg-muted rounded px-1.5 py-0.5">
|
||||
<Text className="text-muted-foreground text-[10px] font-medium tracking-wider uppercase">
|
||||
{sourceLabels[item.source]}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
</Link.Trigger>
|
||||
<Link.Menu>
|
||||
<Link.MenuAction
|
||||
title={t`Remove from history`}
|
||||
icon="trash"
|
||||
destructive
|
||||
onPress={removeFromHistory}
|
||||
/>
|
||||
</Link.Menu>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -6,11 +6,13 @@ import { getWatchCount, getWatchHistory } from "@sofa/core/discovery";
|
||||
import { getOrFetchTitleByTmdbId } from "@sofa/core/metadata";
|
||||
import {
|
||||
getDisplayStatusesByTitleIds,
|
||||
deleteWatch as deleteWatchRecord,
|
||||
getUserTitleInfo,
|
||||
listWatchHistory,
|
||||
logEpisodeWatch,
|
||||
logEpisodeWatchBatch,
|
||||
logMovieWatch,
|
||||
logWatchAt,
|
||||
markAllEpisodesWatched,
|
||||
quickAddTitle,
|
||||
rateTitleStars,
|
||||
@@ -142,3 +144,24 @@ export const history = os.tracking.history.use(authed).handler(({ input, context
|
||||
nextCursor: result.nextCursor,
|
||||
};
|
||||
});
|
||||
|
||||
export const deleteWatch = os.tracking.deleteWatch.use(authed).handler(({ input, context }) => {
|
||||
if (!deleteWatchRecord(context.user.id, input.kind, input.watchId)) {
|
||||
throw new ORPCError("NOT_FOUND", {
|
||||
message: "Watch not found",
|
||||
data: { code: AppErrorCode.WATCH_NOT_FOUND },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const logWatch = os.tracking.logWatch.use(authed).handler(({ input, context }) => {
|
||||
const result = logWatchAt(context.user.id, input.kind, input.id, new Date(input.watchedAt));
|
||||
if (result === "not_found") {
|
||||
const code =
|
||||
input.kind === "movie" ? AppErrorCode.TITLE_NOT_FOUND : AppErrorCode.EPISODE_NOT_FOUND;
|
||||
throw new ORPCError("NOT_FOUND", {
|
||||
message: code === AppErrorCode.TITLE_NOT_FOUND ? "Title not found" : "Episode not found",
|
||||
data: { code },
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -22,6 +22,8 @@ export const implementedRouter = {
|
||||
userInfo: tracking.userInfo,
|
||||
stats: tracking.stats,
|
||||
history: tracking.history,
|
||||
deleteWatch: tracking.deleteWatch,
|
||||
logWatch: tracking.logWatch,
|
||||
},
|
||||
library: {
|
||||
list: library.list,
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { call } from "@orpc/server";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { auth } from "@sofa/auth/server";
|
||||
import { userMovieWatches } from "@sofa/db/schema";
|
||||
import { clearAllTables, insertTitle, insertUser, testDb } 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");
|
||||
insertUser("user-2");
|
||||
insertTitle({ id: "m1", tmdbId: 1, title: "Movie One" });
|
||||
getSession.mockReset();
|
||||
getSession.mockResolvedValue(sessionFor("user-1"));
|
||||
});
|
||||
|
||||
describe("tracking.deleteWatch", () => {
|
||||
test("another user's watch returns NOT_FOUND with WATCH_NOT_FOUND", async () => {
|
||||
testDb
|
||||
.insert(userMovieWatches)
|
||||
.values({
|
||||
id: "w1",
|
||||
userId: "user-2",
|
||||
titleId: "m1",
|
||||
watchedAt: new Date(),
|
||||
source: "manual",
|
||||
})
|
||||
.run();
|
||||
|
||||
await expect(
|
||||
call(implementedRouter.tracking.deleteWatch, { kind: "movie", watchId: "w1" }, ctx),
|
||||
).rejects.toMatchObject({ code: "NOT_FOUND", data: { code: "WATCH_NOT_FOUND" } });
|
||||
expect(testDb.select().from(userMovieWatches).all()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("tracking.logWatch", () => {
|
||||
test("rejects a future watchedAt", async () => {
|
||||
const future = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
||||
await expect(
|
||||
call(
|
||||
implementedRouter.tracking.logWatch,
|
||||
{ kind: "movie", id: "m1", watchedAt: future },
|
||||
ctx,
|
||||
),
|
||||
).rejects.toMatchObject({ code: "BAD_REQUEST" });
|
||||
expect(testDb.select().from(userMovieWatches).all()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("logs a past watch", async () => {
|
||||
await call(
|
||||
implementedRouter.tracking.logWatch,
|
||||
{ kind: "movie", id: "m1", watchedAt: "2024-03-01T12:00:00.000Z" },
|
||||
ctx,
|
||||
);
|
||||
expect(testDb.select().from(userMovieWatches).all()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,26 @@
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
import { IconMovie } from "@tabler/icons-react";
|
||||
import { IconDots, IconMovie, IconTrash } from "@tabler/icons-react";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { thumbHashToUrl } from "@/lib/thumbhash";
|
||||
import type { WatchHistoryItemSchema } from "@sofa/api/schemas";
|
||||
import { formatDate } from "@sofa/i18n/format";
|
||||
|
||||
import { useDeleteWatch } from "./use-delete-watch";
|
||||
|
||||
type WatchHistoryItem = z.infer<typeof WatchHistoryItemSchema>;
|
||||
|
||||
export function HistoryRow({ item }: { item: WatchHistoryItem }) {
|
||||
const { t } = useLingui();
|
||||
const { deleteWatch, pendingId } = useDeleteWatch();
|
||||
|
||||
const sourceLabels = {
|
||||
manual: t`Manual`,
|
||||
@@ -37,42 +47,71 @@ export function HistoryRow({ item }: { item: WatchHistoryItem }) {
|
||||
});
|
||||
|
||||
return (
|
||||
<Link
|
||||
to="/titles/$id"
|
||||
params={{ id: item.title.id }}
|
||||
className="group bg-card/40 hover:bg-card/60 hover:ring-primary/25 flex items-center gap-4 rounded-xl p-3 ring-1 ring-white/[0.06] transition-[background,ring-color] duration-200"
|
||||
>
|
||||
<div className="relative h-[66px] w-11 shrink-0 overflow-hidden rounded-lg ring-1 ring-white/[0.06]">
|
||||
{item.title.posterPath ? (
|
||||
<img
|
||||
src={item.title.posterPath}
|
||||
alt=""
|
||||
className="size-full object-cover"
|
||||
loading="lazy"
|
||||
{...(item.title.posterThumbHash
|
||||
? {
|
||||
style: {
|
||||
background: `url(${thumbHashToUrl(item.title.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="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium">{item.title.title}</div>
|
||||
{subtitle && <div className="text-muted-foreground mt-1 truncate text-xs">{subtitle}</div>}
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col items-end gap-1">
|
||||
<span className="text-muted-foreground text-xs tabular-nums">{time}</span>
|
||||
<span className="bg-muted text-muted-foreground rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wider uppercase">
|
||||
{sourceLabels[item.source]}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="relative">
|
||||
<Link
|
||||
to="/titles/$id"
|
||||
params={{ id: item.title.id }}
|
||||
className="group bg-card/40 hover:bg-card/60 hover:ring-primary/25 flex items-center gap-4 rounded-xl p-3 pe-12 ring-1 ring-white/[0.06] transition-[background,ring-color] duration-200"
|
||||
>
|
||||
<div className="relative h-[66px] w-11 shrink-0 overflow-hidden rounded-lg ring-1 ring-white/[0.06]">
|
||||
{item.title.posterPath ? (
|
||||
<img
|
||||
src={item.title.posterPath}
|
||||
alt=""
|
||||
className="size-full object-cover"
|
||||
loading="lazy"
|
||||
{...(item.title.posterThumbHash
|
||||
? {
|
||||
style: {
|
||||
background: `url(${thumbHashToUrl(item.title.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="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium">{item.title.title}</div>
|
||||
{subtitle && (
|
||||
<div className="text-muted-foreground mt-1 truncate text-xs">{subtitle}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col items-end gap-1">
|
||||
<span className="text-muted-foreground text-xs tabular-nums">{time}</span>
|
||||
<span className="bg-muted text-muted-foreground rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wider uppercase">
|
||||
{sourceLabels[item.source]}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label={t`More actions`}
|
||||
className="absolute end-2 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
<IconDots aria-hidden={true} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
className="cursor-pointer"
|
||||
disabled={pendingId !== null}
|
||||
onClick={() => deleteWatch(item.kind, item.watchId)}
|
||||
>
|
||||
<IconTrash aria-hidden={true} />
|
||||
{t`Remove from history`}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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";
|
||||
|
||||
/** Remove a single watch from history, with toast + cache refresh. */
|
||||
export function useDeleteWatch() {
|
||||
const { t } = useLingui();
|
||||
const queryClient = useQueryClient();
|
||||
// Handlers live on the hook-level options so the mutation stays pending until
|
||||
// the tracking queries have refetched, which blocks duplicate clicks.
|
||||
const { mutate, variables, isPending } = useMutation(
|
||||
orpc.tracking.deleteWatch.mutationOptions({
|
||||
onSuccess: async () => {
|
||||
toast.success(t`Removed from history`);
|
||||
await invalidateTrackingQueries(queryClient);
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(getErrorMessage(err, t`Failed to remove from history`));
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
function deleteWatch(kind: "movie" | "episode", watchId: string) {
|
||||
if (isPending) return;
|
||||
mutate({ kind, watchId });
|
||||
}
|
||||
|
||||
const pendingId = isPending ? (variables?.watchId ?? null) : null;
|
||||
|
||||
return { deleteWatch, pendingId };
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
import { IconHistory } from "@tabler/icons-react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { getErrorMessage } from "@/lib/error-messages";
|
||||
import { orpc } from "@/lib/orpc/client";
|
||||
import { invalidateTrackingQueries } from "@/lib/orpc/invalidate";
|
||||
|
||||
function todayLocal() {
|
||||
const now = new Date();
|
||||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(now.getDate()).padStart(2, "0");
|
||||
return `${now.getFullYear()}-${month}-${day}`;
|
||||
}
|
||||
|
||||
/** Log a watch of a movie on a chosen date (and optional time). */
|
||||
export function LogWatchDialog({ titleId }: { titleId: string }) {
|
||||
const { t } = useLingui();
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [date, setDate] = useState(todayLocal);
|
||||
const [time, setTime] = useState("");
|
||||
|
||||
const { mutate, isPending } = useMutation(
|
||||
orpc.tracking.logWatch.mutationOptions({
|
||||
onSuccess: async () => {
|
||||
toast.success(t`Watch logged`);
|
||||
setOpen(false);
|
||||
await invalidateTrackingQueries(queryClient);
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(getErrorMessage(err, t`Failed to log watch`));
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
function handleOpenChange(next: boolean) {
|
||||
if (next) {
|
||||
setDate(todayLocal());
|
||||
setTime("");
|
||||
}
|
||||
setOpen(next);
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!date || isPending) return;
|
||||
// Date-only watches land at local noon, matching imported date-only watches.
|
||||
const watchedAt = new Date(`${date}T${time || "12:00"}:00`);
|
||||
if (Number.isNaN(watchedAt.getTime())) return;
|
||||
mutate({ kind: "movie", id: titleId, watchedAt: watchedAt.toISOString() });
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="h-9 rounded-lg px-4 text-sm active:scale-[0.97]"
|
||||
onClick={() => handleOpenChange(true)}
|
||||
>
|
||||
<IconHistory aria-hidden={true} className="size-3.5" />
|
||||
{t`Log a watch…`}
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent>
|
||||
<form onSubmit={handleSubmit} className="grid gap-4">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t`Log a watch`}</DialogTitle>
|
||||
<DialogDescription>{t`Record when you watched this.`}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-3">
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="log-watch-date">{t`Date`}</Label>
|
||||
<Input
|
||||
id="log-watch-date"
|
||||
type="date"
|
||||
required
|
||||
value={date}
|
||||
max={todayLocal()}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="log-watch-time">{t`Time (optional)`}</Label>
|
||||
<Input
|
||||
id="log-watch-time"
|
||||
type="time"
|
||||
value={time}
|
||||
onChange={(e) => setTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isPending || !date}>
|
||||
{t`Log watch`}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -4,13 +4,14 @@ import { IconCheck, IconX } from "@tabler/icons-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
import { LogWatchDialog } from "./log-watch-dialog";
|
||||
import { StarRating } from "./star-rating";
|
||||
import { StatusButton } from "./status-button";
|
||||
import { useTitleContext, useTitleUserInfo } from "./title-context";
|
||||
import { useTitleActions } from "./use-title-actions";
|
||||
|
||||
export function TitleActions() {
|
||||
const { titleType } = useTitleContext();
|
||||
const { titleType, titleId } = useTitleContext();
|
||||
const { userStatus, userRating } = useTitleUserInfo();
|
||||
const { handleStatusChange, handleRating, handleWatchMovie, handleUnwatchMovie } =
|
||||
useTitleActions();
|
||||
@@ -39,6 +40,7 @@ export function TitleActions() {
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{titleType === "movie" && <LogWatchDialog titleId={titleId} />}
|
||||
<Separator orientation="vertical" className="bg-border/50 mx-0.5 my-auto h-6" />
|
||||
<StarRating value={userRating ?? 0} onChange={handleRating} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Remove one watch
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: POST
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: Remove a single watch record (one play of a movie or episode) belonging
|
||||
to the current user.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/tracking/history/delete","method":"post"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Log a watch at a date
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
method: POST
|
||||
webhook: false
|
||||
toc: []
|
||||
structuredData:
|
||||
headings: []
|
||||
contents:
|
||||
- content: Log a watch of a movie (title ID) or an episode (episode ID) at a
|
||||
specific date and time. Dates in the future or before 1900 are
|
||||
rejected.
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
export default function Layout(props) {
|
||||
const { OpenAPIPage } = props.components ?? {};
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<OpenAPIPage document="./public/openapi.json" operations={[{"path":"/tracking/log","method":"post"}]} showDescription />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2533,6 +2533,258 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/tracking/history/delete": {
|
||||
"post": {
|
||||
"operationId": "tracking.deleteWatch",
|
||||
"summary": "Remove one watch",
|
||||
"description": "Remove a single watch record (one play of a movie or episode) belonging to the current user.",
|
||||
"tags": [
|
||||
"Tracking"
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"movie",
|
||||
"episode"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Whether the watch is a movie or an episode"
|
||||
},
|
||||
"watchId": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Watch record ID (from the history list)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"watchId"
|
||||
],
|
||||
"description": "Identifies a single watch record to remove"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
},
|
||||
"404": {
|
||||
"description": "404",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"defined": {
|
||||
"const": true
|
||||
},
|
||||
"code": {
|
||||
"const": "NOT_FOUND"
|
||||
},
|
||||
"status": {
|
||||
"const": 404
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"default": "Watch not found"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"const": "WATCH_NOT_FOUND"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"defined",
|
||||
"code",
|
||||
"status",
|
||||
"message",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"defined": {
|
||||
"const": false
|
||||
},
|
||||
"code": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "number"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
"required": [
|
||||
"defined",
|
||||
"code",
|
||||
"status",
|
||||
"message"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"session": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/tracking/log": {
|
||||
"post": {
|
||||
"operationId": "tracking.logWatch",
|
||||
"summary": "Log a watch at a date",
|
||||
"description": "Log a watch of a movie (title ID) or an episode (episode ID) at a specific date and time. Dates in the future or before 1900 are rejected.",
|
||||
"tags": [
|
||||
"Tracking"
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"movie",
|
||||
"episode"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Whether id is a movie title ID or an episode ID"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Movie title ID or episode ID"
|
||||
},
|
||||
"watchedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "ISO 8601 timestamp of when it was watched"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"id",
|
||||
"watchedAt"
|
||||
],
|
||||
"description": "A watch to log at a specific date and time"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
},
|
||||
"404": {
|
||||
"description": "404",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"defined": {
|
||||
"const": true
|
||||
},
|
||||
"code": {
|
||||
"const": "NOT_FOUND"
|
||||
},
|
||||
"status": {
|
||||
"const": 404
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"default": "Title or episode not found"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"enum": [
|
||||
"TITLE_NOT_FOUND",
|
||||
"EPISODE_NOT_FOUND"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"defined",
|
||||
"code",
|
||||
"status",
|
||||
"message",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"defined": {
|
||||
"const": false
|
||||
},
|
||||
"code": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "number"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {}
|
||||
},
|
||||
"required": [
|
||||
"defined",
|
||||
"code",
|
||||
"status",
|
||||
"message"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"session": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/library": {
|
||||
"get": {
|
||||
"operationId": "library.list",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ContinueWatchingOutput,
|
||||
CreateImportJobInput,
|
||||
CreateIntegrationInput,
|
||||
DeleteWatchInput,
|
||||
DiscoverInput,
|
||||
DiscoverOutput,
|
||||
DiscoverRecommendationsOutput,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
LibraryListInput,
|
||||
LibraryListOutput,
|
||||
LibraryStatsOutput,
|
||||
LogWatchInput,
|
||||
MediaTypeParam,
|
||||
PageParam,
|
||||
PaginatedInput,
|
||||
@@ -197,6 +199,42 @@ export const contract = {
|
||||
})
|
||||
.input(WatchHistoryListInput)
|
||||
.output(WatchHistoryListOutput),
|
||||
deleteWatch: oc
|
||||
.route({
|
||||
method: "POST",
|
||||
path: "/tracking/history/delete",
|
||||
tags: ["Tracking"],
|
||||
summary: "Remove one watch",
|
||||
description:
|
||||
"Remove a single watch record (one play of a movie or episode) belonging to the current user.",
|
||||
})
|
||||
.input(DeleteWatchInput)
|
||||
.output(z.void())
|
||||
.errors({
|
||||
NOT_FOUND: {
|
||||
message: "Watch not found",
|
||||
data: appErrorData(AppErrorCode.WATCH_NOT_FOUND),
|
||||
},
|
||||
}),
|
||||
logWatch: oc
|
||||
.route({
|
||||
method: "POST",
|
||||
path: "/tracking/log",
|
||||
tags: ["Tracking"],
|
||||
summary: "Log a watch at a date",
|
||||
description:
|
||||
"Log a watch of a movie (title ID) or an episode (episode ID) at a specific date and time. Dates in the future or before 1900 are rejected.",
|
||||
})
|
||||
.input(LogWatchInput)
|
||||
.output(z.void())
|
||||
.errors({
|
||||
NOT_FOUND: {
|
||||
message: "Title or episode not found",
|
||||
data: z.object({
|
||||
code: z.enum([AppErrorCode.TITLE_NOT_FOUND, AppErrorCode.EPISODE_NOT_FOUND]),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
// ─── Library ────────────────────────────────────────────────
|
||||
|
||||
@@ -2,6 +2,8 @@ import { z } from "zod";
|
||||
|
||||
export const AppErrorCode = {
|
||||
TITLE_NOT_FOUND: "TITLE_NOT_FOUND",
|
||||
EPISODE_NOT_FOUND: "EPISODE_NOT_FOUND",
|
||||
WATCH_NOT_FOUND: "WATCH_NOT_FOUND",
|
||||
PERSON_NOT_FOUND: "PERSON_NOT_FOUND",
|
||||
INTEGRATION_NOT_FOUND: "INTEGRATION_NOT_FOUND",
|
||||
BACKUP_NOT_FOUND: "BACKUP_NOT_FOUND",
|
||||
|
||||
@@ -147,6 +147,31 @@ export const WatchHistoryListInput = z
|
||||
})
|
||||
.meta({ description: "Filters and pagination for watch history" });
|
||||
|
||||
const MIN_WATCHED_AT_MS = Date.UTC(1900, 0, 1);
|
||||
const MAX_FUTURE_SKEW_MS = 5 * 60 * 1000;
|
||||
|
||||
export const DeleteWatchInput = z
|
||||
.object({
|
||||
kind: z.enum(["movie", "episode"]).describe("Whether the watch is a movie or an episode"),
|
||||
watchId: z.string().min(1).describe("Watch record ID (from the history list)"),
|
||||
})
|
||||
.meta({ description: "Identifies a single watch record to remove" });
|
||||
|
||||
export const LogWatchInput = z
|
||||
.object({
|
||||
kind: z.enum(["movie", "episode"]).describe("Whether id is a movie title ID or an episode ID"),
|
||||
id: z.string().min(1).describe("Movie title ID or episode ID"),
|
||||
watchedAt: z
|
||||
.string()
|
||||
.datetime({ offset: true })
|
||||
.refine((value) => {
|
||||
const ms = Date.parse(value);
|
||||
return ms >= MIN_WATCHED_AT_MS && ms <= Date.now() + MAX_FUTURE_SKEW_MS;
|
||||
}, "watchedAt must be between 1900-01-01 and now")
|
||||
.describe("ISO 8601 timestamp of when it was watched"),
|
||||
})
|
||||
.meta({ description: "A watch to log at a specific date and time" });
|
||||
|
||||
export const WatchHistoryItemSchema = z
|
||||
.object({
|
||||
watchId: z.string().describe("Watch record ID"),
|
||||
|
||||
@@ -4,9 +4,12 @@ import {
|
||||
batchInsertEpisodeWatchesTransaction,
|
||||
batchInsertMissingEpisodeWatches,
|
||||
countDistinctEpisodeWatches,
|
||||
countMovieWatches,
|
||||
deleteAllEpisodeWatchesForTitle,
|
||||
deleteEpisodeWatch,
|
||||
deleteEpisodeWatchById,
|
||||
deleteEpisodeWatches,
|
||||
deleteMovieWatchById,
|
||||
deleteMovieWatches,
|
||||
deleteRating,
|
||||
deleteTitleStatus,
|
||||
@@ -124,12 +127,8 @@ export function markAllEpisodesWatched(
|
||||
setTitleStatus(userId, titleId, "in_progress", source);
|
||||
}
|
||||
|
||||
export function unwatchEpisode(userId: string, episodeId: string) {
|
||||
deleteEpisodeWatch(userId, episodeId);
|
||||
|
||||
const titleId = getEpisodeTitleId(episodeId);
|
||||
if (!titleId) return;
|
||||
|
||||
/** After removing episode watches: an in-progress show with none left goes back to watchlist. */
|
||||
function downgradeShowIfNoEpisodeWatches(userId: string, titleId: string) {
|
||||
const existing = getTitleStatus(userId, titleId);
|
||||
if (!existing || existing.status !== "in_progress") return;
|
||||
|
||||
@@ -141,6 +140,23 @@ export function unwatchEpisode(userId: string, episodeId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/** After removing all movie watches: a tracked movie falls back to watchlist. */
|
||||
function resetMovieToWatchlist(userId: string, titleId: string) {
|
||||
const existing = getTitleStatus(userId, titleId);
|
||||
if (existing && existing.status !== "watchlist") {
|
||||
setTitleStatus(userId, titleId, "watchlist");
|
||||
}
|
||||
}
|
||||
|
||||
export function unwatchEpisode(userId: string, episodeId: string) {
|
||||
deleteEpisodeWatch(userId, episodeId);
|
||||
|
||||
const titleId = getEpisodeTitleId(episodeId);
|
||||
if (!titleId) return;
|
||||
|
||||
downgradeShowIfNoEpisodeWatches(userId, titleId);
|
||||
}
|
||||
|
||||
export function unwatchSeason(userId: string, seasonId: string) {
|
||||
const epIds = getSeasonEpisodeIds(seasonId);
|
||||
if (epIds.length > 0) {
|
||||
@@ -150,24 +166,49 @@ export function unwatchSeason(userId: string, seasonId: string) {
|
||||
const season = getSeasonById(seasonId);
|
||||
if (!season) return;
|
||||
|
||||
const existing = getTitleStatus(userId, season.titleId);
|
||||
if (!existing || existing.status !== "in_progress") return;
|
||||
|
||||
// If no episodes remain watched, downgrade to watchlist
|
||||
const allEpIds = getAllEpisodeIdsForTitle(season.titleId);
|
||||
const watchCount = countDistinctEpisodeWatches(userId, allEpIds);
|
||||
if (watchCount === 0) {
|
||||
setTitleStatus(userId, season.titleId, "watchlist");
|
||||
}
|
||||
downgradeShowIfNoEpisodeWatches(userId, season.titleId);
|
||||
}
|
||||
|
||||
export function unwatchMovie(userId: string, titleId: string) {
|
||||
deleteMovieWatches(userId, titleId);
|
||||
resetMovieToWatchlist(userId, titleId);
|
||||
}
|
||||
|
||||
const existing = getTitleStatus(userId, titleId);
|
||||
if (existing && existing.status !== "watchlist") {
|
||||
setTitleStatus(userId, titleId, "watchlist");
|
||||
/** Remove a single watch owned by the user. Returns false if it doesn't exist or isn't theirs. */
|
||||
export function deleteWatch(userId: string, kind: "movie" | "episode", watchId: string): boolean {
|
||||
if (kind === "movie") {
|
||||
const deleted = deleteMovieWatchById(userId, watchId);
|
||||
if (!deleted) return false;
|
||||
if (countMovieWatches(userId, deleted.titleId) === 0) {
|
||||
resetMovieToWatchlist(userId, deleted.titleId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const deleted = deleteEpisodeWatchById(userId, watchId);
|
||||
if (!deleted) return false;
|
||||
const titleId = getEpisodeTitleId(deleted.episodeId);
|
||||
if (titleId) downgradeShowIfNoEpisodeWatches(userId, titleId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Log a manual watch at a given time. `id` is a movie title id or an episode id. */
|
||||
export function logWatchAt(
|
||||
userId: string,
|
||||
kind: "movie" | "episode",
|
||||
id: string,
|
||||
watchedAt: Date,
|
||||
): "ok" | "not_found" {
|
||||
if (kind === "movie") {
|
||||
const title = getTitleById(id);
|
||||
if (!title || title.type !== "movie") return "not_found";
|
||||
logMovieWatch(userId, id, "manual", watchedAt);
|
||||
return "ok";
|
||||
}
|
||||
|
||||
if (!getEpisodeTitleId(id)) return "not_found";
|
||||
logEpisodeWatch(userId, id, "manual", watchedAt);
|
||||
return "ok";
|
||||
}
|
||||
|
||||
export function unwatchSeries(userId: string, titleId: string) {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
|
||||
import { userEpisodeWatches, userMovieWatches, userTitleStatus } from "@sofa/db/schema";
|
||||
import {
|
||||
clearAllTables,
|
||||
insertStatus,
|
||||
insertTitle,
|
||||
insertTvShow,
|
||||
insertUser,
|
||||
testDb,
|
||||
} from "@sofa/test/db";
|
||||
|
||||
import { deleteWatch, logWatchAt } from "../src/tracking";
|
||||
|
||||
function movieWatchRows(userId: string, titleId: string) {
|
||||
return testDb
|
||||
.select()
|
||||
.from(userMovieWatches)
|
||||
.where(eq(userMovieWatches.titleId, titleId))
|
||||
.all()
|
||||
.filter((r) => r.userId === userId);
|
||||
}
|
||||
|
||||
function episodeWatchRows(userId: string) {
|
||||
return testDb
|
||||
.select()
|
||||
.from(userEpisodeWatches)
|
||||
.all()
|
||||
.filter((r) => r.userId === userId);
|
||||
}
|
||||
|
||||
function statusOf(userId: string, titleId: string) {
|
||||
return testDb
|
||||
.select()
|
||||
.from(userTitleStatus)
|
||||
.all()
|
||||
.find((r) => r.userId === userId && r.titleId === titleId)?.status;
|
||||
}
|
||||
|
||||
function addMovieWatch(id: string, userId: string, titleId: string) {
|
||||
testDb
|
||||
.insert(userMovieWatches)
|
||||
.values({ id, userId, titleId, watchedAt: new Date(), source: "manual" })
|
||||
.run();
|
||||
}
|
||||
|
||||
function addEpisodeWatch(id: string, userId: string, episodeId: string) {
|
||||
testDb
|
||||
.insert(userEpisodeWatches)
|
||||
.values({ id, userId, episodeId, watchedAt: new Date(), source: "manual" })
|
||||
.run();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
clearAllTables();
|
||||
insertUser("user-1");
|
||||
insertUser("user-2");
|
||||
insertTitle({ id: "m1", tmdbId: 1, title: "Movie One" });
|
||||
insertTvShow("tv-1", 100, 1, 3);
|
||||
});
|
||||
|
||||
describe("deleteWatch", () => {
|
||||
test("deleting one of two movie watches keeps completed", () => {
|
||||
insertStatus("user-1", "m1", "completed");
|
||||
addMovieWatch("w1", "user-1", "m1");
|
||||
addMovieWatch("w2", "user-1", "m1");
|
||||
|
||||
expect(deleteWatch("user-1", "movie", "w1")).toBe(true);
|
||||
expect(movieWatchRows("user-1", "m1")).toHaveLength(1);
|
||||
expect(statusOf("user-1", "m1")).toBe("completed");
|
||||
});
|
||||
|
||||
test("deleting the last movie watch sets watchlist", () => {
|
||||
insertStatus("user-1", "m1", "completed");
|
||||
addMovieWatch("w1", "user-1", "m1");
|
||||
|
||||
expect(deleteWatch("user-1", "movie", "w1")).toBe(true);
|
||||
expect(movieWatchRows("user-1", "m1")).toHaveLength(0);
|
||||
expect(statusOf("user-1", "m1")).toBe("watchlist");
|
||||
});
|
||||
|
||||
test("deleting the only episode watch of an in-progress show sets watchlist", () => {
|
||||
insertStatus("user-1", "tv-1", "in_progress");
|
||||
addEpisodeWatch("e1", "user-1", "tv-1-s1e1");
|
||||
|
||||
expect(deleteWatch("user-1", "episode", "e1")).toBe(true);
|
||||
expect(statusOf("user-1", "tv-1")).toBe("watchlist");
|
||||
});
|
||||
|
||||
test("deleting one of two episode watches keeps in_progress", () => {
|
||||
insertStatus("user-1", "tv-1", "in_progress");
|
||||
addEpisodeWatch("e1", "user-1", "tv-1-s1e1");
|
||||
addEpisodeWatch("e2", "user-1", "tv-1-s1e2");
|
||||
|
||||
expect(deleteWatch("user-1", "episode", "e1")).toBe(true);
|
||||
expect(episodeWatchRows("user-1")).toHaveLength(1);
|
||||
expect(statusOf("user-1", "tv-1")).toBe("in_progress");
|
||||
});
|
||||
|
||||
test("another user's watch is not deleted", () => {
|
||||
insertStatus("user-1", "m1", "completed");
|
||||
addMovieWatch("w1", "user-1", "m1");
|
||||
addEpisodeWatch("e1", "user-1", "tv-1-s1e1");
|
||||
|
||||
expect(deleteWatch("user-2", "movie", "w1")).toBe(false);
|
||||
expect(deleteWatch("user-2", "episode", "e1")).toBe(false);
|
||||
expect(movieWatchRows("user-1", "m1")).toHaveLength(1);
|
||||
expect(episodeWatchRows("user-1")).toHaveLength(1);
|
||||
expect(statusOf("user-1", "m1")).toBe("completed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("logWatchAt", () => {
|
||||
test("logs a movie on a past date and sets completed", () => {
|
||||
const at = new Date("2024-03-01T12:00:00Z");
|
||||
expect(logWatchAt("user-1", "movie", "m1", at)).toBe("ok");
|
||||
const rows = movieWatchRows("user-1", "m1");
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]?.watchedAt.getTime()).toBe(at.getTime());
|
||||
expect(statusOf("user-1", "m1")).toBe("completed");
|
||||
});
|
||||
|
||||
test("allows a rewatch of an already watched movie", () => {
|
||||
insertStatus("user-1", "m1", "completed");
|
||||
addMovieWatch("w1", "user-1", "m1");
|
||||
|
||||
expect(logWatchAt("user-1", "movie", "m1", new Date("2024-03-01T12:00:00Z"))).toBe("ok");
|
||||
expect(movieWatchRows("user-1", "m1")).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("logs an episode and moves watchlist to in_progress", () => {
|
||||
insertStatus("user-1", "tv-1", "watchlist");
|
||||
expect(logWatchAt("user-1", "episode", "tv-1-s1e1", new Date("2024-03-01T12:00:00Z"))).toBe(
|
||||
"ok",
|
||||
);
|
||||
expect(episodeWatchRows("user-1")).toHaveLength(1);
|
||||
expect(statusOf("user-1", "tv-1")).toBe("in_progress");
|
||||
});
|
||||
|
||||
test("returns not_found for a TV title as movie, or unknown ids", () => {
|
||||
const at = new Date("2024-03-01T12:00:00Z");
|
||||
expect(logWatchAt("user-1", "movie", "tv-1", at)).toBe("not_found");
|
||||
expect(logWatchAt("user-1", "movie", "nope", at)).toBe("not_found");
|
||||
expect(logWatchAt("user-1", "episode", "nope", at)).toBe("not_found");
|
||||
expect(movieWatchRows("user-1", "tv-1")).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -337,6 +337,36 @@ export function deleteMovieWatches(userId: string, titleId: string): void {
|
||||
.run();
|
||||
}
|
||||
|
||||
export function deleteMovieWatchById(userId: string, watchId: string): { titleId: string } | null {
|
||||
const row = db
|
||||
.delete(userMovieWatches)
|
||||
.where(and(eq(userMovieWatches.id, watchId), eq(userMovieWatches.userId, userId)))
|
||||
.returning({ titleId: userMovieWatches.titleId })
|
||||
.get();
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
export function deleteEpisodeWatchById(
|
||||
userId: string,
|
||||
watchId: string,
|
||||
): { episodeId: string } | null {
|
||||
const row = db
|
||||
.delete(userEpisodeWatches)
|
||||
.where(and(eq(userEpisodeWatches.id, watchId), eq(userEpisodeWatches.userId, userId)))
|
||||
.returning({ episodeId: userEpisodeWatches.episodeId })
|
||||
.get();
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
export function countMovieWatches(userId: string, titleId: string): number {
|
||||
const row = db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(userMovieWatches)
|
||||
.where(and(eq(userMovieWatches.userId, userId), eq(userMovieWatches.titleId, titleId)))
|
||||
.get();
|
||||
return row?.count ?? 0;
|
||||
}
|
||||
|
||||
export function deleteAllEpisodeWatchesForTitle(userId: string, titleId: string): void {
|
||||
const titleEpisodeIds = db
|
||||
.select({ id: episodes.id })
|
||||
|
||||
@@ -14,6 +14,8 @@ export function getAppErrorCode(error: unknown): AppErrorCode | null {
|
||||
|
||||
const APP_ERROR_MESSAGES: Record<AppErrorCode, MessageDescriptor> = {
|
||||
TITLE_NOT_FOUND: msg`Title not found`,
|
||||
EPISODE_NOT_FOUND: msg`Episode not found`,
|
||||
WATCH_NOT_FOUND: msg`Watch not found`,
|
||||
PERSON_NOT_FOUND: msg`Person not found`,
|
||||
INTEGRATION_NOT_FOUND: msg`Integration not found`,
|
||||
BACKUP_NOT_FOUND: msg`Backup not found`,
|
||||
|
||||
@@ -962,6 +962,10 @@ msgstr "نسخ احتياطية لقاعدة البيانات"
|
||||
msgid "Database restored. Reloading..."
|
||||
msgstr "تم استعادة قاعدة البيانات. جارٍ إعادة التحميل..."
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/library/sort-menu.tsx
|
||||
#: apps/web/src/components/library/library-toolbar.tsx
|
||||
msgid "Date Added"
|
||||
@@ -1170,6 +1174,10 @@ msgstr "الحلقة {episodeNumber}"
|
||||
msgid "Episode {episodeNumber}, {episodeLabel}"
|
||||
msgstr "الحلقة {episodeNumber}، {episodeLabel}"
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Episode not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/titles/season-accordion.tsx
|
||||
#: apps/native/src/hooks/use-title-actions.ts
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
@@ -1309,6 +1317,10 @@ msgstr "فشل تحميل إعدادات جدول النسخ الاحتياطي"
|
||||
msgid "Failed to load title"
|
||||
msgstr "فشل تحميل العنوان"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Failed to log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
#: apps/web/src/components/titles/use-title-actions.ts
|
||||
msgid "Failed to mark all episodes as watched"
|
||||
@@ -1365,6 +1377,11 @@ msgstr "فشل مسح ذاكرة التخزين المؤقت للبيانات ا
|
||||
msgid "Failed to regenerate {label} URL"
|
||||
msgstr "فشل إعادة إنشاء رابط {label}"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Failed to remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
msgid "Failed to remove from library"
|
||||
msgstr "فشل الإزالة من المكتبة"
|
||||
@@ -1863,6 +1880,18 @@ msgstr "جارٍ التحميل"
|
||||
msgid "Loading…"
|
||||
msgstr "جارٍ التحميل…"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch…"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Login failed"
|
||||
msgstr "فشل تسجيل الدخول"
|
||||
@@ -2038,6 +2067,10 @@ msgstr "الاثنين"
|
||||
msgid "More"
|
||||
msgstr "المزيد"
|
||||
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "More actions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
msgid "More Like This"
|
||||
msgstr "المزيد من هذا القبيل"
|
||||
@@ -2676,6 +2709,10 @@ msgstr "موصى به"
|
||||
msgid "Recommended for You"
|
||||
msgstr "موصى به لك"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Record when you watched this."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Redirecting…"
|
||||
msgstr "جارٍ إعادة التوجيه…"
|
||||
@@ -2739,6 +2776,11 @@ msgstr "تاريخ الإصدار"
|
||||
msgid "Remove"
|
||||
msgstr "إزالة"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "Remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
@@ -2774,6 +2816,11 @@ msgstr "إزالة الصورة"
|
||||
msgid "Remove profile picture"
|
||||
msgstr "إزالة صورة الملف الشخصي"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Removed from history"
|
||||
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
|
||||
@@ -3407,6 +3454,10 @@ msgstr "هذا العام"
|
||||
msgid "Thursday"
|
||||
msgstr "الخميس"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Time (optional)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Time:"
|
||||
msgstr "الوقت:"
|
||||
@@ -3748,6 +3799,14 @@ msgstr "مشاهدة الكل"
|
||||
msgid "Watch history"
|
||||
msgstr "سجل المشاهدة"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Watch logged"
|
||||
msgstr ""
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Watch not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/title-availability.tsx
|
||||
#~ msgid "Watch on {name}"
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -962,6 +962,10 @@ msgstr "Datenbank-Backups"
|
||||
msgid "Database restored. Reloading..."
|
||||
msgstr "Datenbank wiederhergestellt. Lade neu..."
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/library/sort-menu.tsx
|
||||
#: apps/web/src/components/library/library-toolbar.tsx
|
||||
msgid "Date Added"
|
||||
@@ -1170,6 +1174,10 @@ msgstr "Folge {episodeNumber}"
|
||||
msgid "Episode {episodeNumber}, {episodeLabel}"
|
||||
msgstr "Folge {episodeNumber}, {episodeLabel}"
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Episode not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/titles/season-accordion.tsx
|
||||
#: apps/native/src/hooks/use-title-actions.ts
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
@@ -1309,6 +1317,10 @@ msgstr "Backup-Zeitplaneinstellungen konnten nicht geladen werden"
|
||||
msgid "Failed to load title"
|
||||
msgstr "Titel konnte nicht geladen werden"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Failed to log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
#: apps/web/src/components/titles/use-title-actions.ts
|
||||
msgid "Failed to mark all episodes as watched"
|
||||
@@ -1365,6 +1377,11 @@ msgstr "Metadaten-Cache konnte nicht geleert werden"
|
||||
msgid "Failed to regenerate {label} URL"
|
||||
msgstr "{label}-URL konnte nicht neu generiert werden"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Failed to remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
msgid "Failed to remove from library"
|
||||
msgstr "Konnte nicht aus der Bibliothek entfernt werden"
|
||||
@@ -1863,6 +1880,18 @@ msgstr "Wird geladen"
|
||||
msgid "Loading…"
|
||||
msgstr "Lade..."
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch…"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Login failed"
|
||||
msgstr "Anmeldung fehlgeschlagen"
|
||||
@@ -2038,6 +2067,10 @@ msgstr "Montag"
|
||||
msgid "More"
|
||||
msgstr "Mehr"
|
||||
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "More actions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
msgid "More Like This"
|
||||
msgstr "Ähnliche Titel"
|
||||
@@ -2676,6 +2709,10 @@ msgstr "Empfohlen"
|
||||
msgid "Recommended for You"
|
||||
msgstr "Für dich empfohlen"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Record when you watched this."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Redirecting…"
|
||||
msgstr "Weiterleiten..."
|
||||
@@ -2739,6 +2776,11 @@ msgstr "Erscheinungsdatum"
|
||||
msgid "Remove"
|
||||
msgstr "Entfernen"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "Remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
@@ -2774,6 +2816,11 @@ msgstr "Bild entfernen"
|
||||
msgid "Remove profile picture"
|
||||
msgstr "Profilbild entfernen"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Removed from history"
|
||||
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
|
||||
@@ -3407,6 +3454,10 @@ msgstr "Dieses Jahr"
|
||||
msgid "Thursday"
|
||||
msgstr "Donnerstag"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Time (optional)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Time:"
|
||||
msgstr "Uhrzeit:"
|
||||
@@ -3748,6 +3799,14 @@ msgstr "Alle als gesehen markieren"
|
||||
msgid "Watch history"
|
||||
msgstr "Verlauf"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Watch logged"
|
||||
msgstr ""
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Watch not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/title-availability.tsx
|
||||
#~ msgid "Watch on {name}"
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -962,6 +962,10 @@ msgstr ""
|
||||
msgid "Database restored. Reloading..."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Date"
|
||||
msgstr "Date"
|
||||
|
||||
#: apps/native/src/components/library/sort-menu.tsx
|
||||
#: apps/web/src/components/library/library-toolbar.tsx
|
||||
msgid "Date Added"
|
||||
@@ -1170,6 +1174,10 @@ msgstr "Episode {episodeNumber}"
|
||||
msgid "Episode {episodeNumber}, {episodeLabel}"
|
||||
msgstr "Episode {episodeNumber}, {episodeLabel}"
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Episode not found"
|
||||
msgstr "Episode not found"
|
||||
|
||||
#: apps/native/src/components/titles/season-accordion.tsx
|
||||
#: apps/native/src/hooks/use-title-actions.ts
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
@@ -1309,6 +1317,10 @@ msgstr "Failed to load backup schedule settings"
|
||||
msgid "Failed to load title"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Failed to log watch"
|
||||
msgstr "Failed to log watch"
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
#: apps/web/src/components/titles/use-title-actions.ts
|
||||
msgid "Failed to mark all episodes as watched"
|
||||
@@ -1365,6 +1377,11 @@ msgstr ""
|
||||
msgid "Failed to regenerate {label} URL"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Failed to remove from history"
|
||||
msgstr "Failed to remove from history"
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
msgid "Failed to remove from library"
|
||||
msgstr ""
|
||||
@@ -1863,6 +1880,18 @@ msgstr "Loading"
|
||||
msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch"
|
||||
msgstr "Log a watch"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch…"
|
||||
msgstr "Log a watch…"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log watch"
|
||||
msgstr "Log watch"
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Login failed"
|
||||
msgstr "Login failed"
|
||||
@@ -2038,6 +2067,10 @@ msgstr ""
|
||||
msgid "More"
|
||||
msgstr "More"
|
||||
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "More actions"
|
||||
msgstr "More actions"
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
msgid "More Like This"
|
||||
msgstr ""
|
||||
@@ -2676,6 +2709,10 @@ msgstr ""
|
||||
msgid "Recommended for You"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Record when you watched this."
|
||||
msgstr "Record when you watched this."
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Redirecting…"
|
||||
msgstr ""
|
||||
@@ -2739,6 +2776,11 @@ msgstr "Release Date"
|
||||
msgid "Remove"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "Remove from history"
|
||||
msgstr "Remove from history"
|
||||
|
||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
@@ -2774,6 +2816,11 @@ msgstr ""
|
||||
msgid "Remove profile picture"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Removed from history"
|
||||
msgstr "Removed from history"
|
||||
|
||||
#: apps/native/src/hooks/use-title-actions.ts
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
#: apps/web/src/components/titles/use-title-actions.ts
|
||||
@@ -3407,6 +3454,10 @@ msgstr ""
|
||||
msgid "Thursday"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Time (optional)"
|
||||
msgstr "Time (optional)"
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Time:"
|
||||
msgstr ""
|
||||
@@ -3748,6 +3799,14 @@ msgstr ""
|
||||
msgid "Watch history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Watch logged"
|
||||
msgstr "Watch logged"
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Watch not found"
|
||||
msgstr "Watch not found"
|
||||
|
||||
#: apps/web/src/components/titles/title-availability.tsx
|
||||
#~ msgid "Watch on {name}"
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -962,6 +962,10 @@ msgstr "Copias de seguridad de la base de datos"
|
||||
msgid "Database restored. Reloading..."
|
||||
msgstr "Base de datos restaurada. Recargando..."
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/library/sort-menu.tsx
|
||||
#: apps/web/src/components/library/library-toolbar.tsx
|
||||
msgid "Date Added"
|
||||
@@ -1170,6 +1174,10 @@ msgstr "Episodio {episodeNumber}"
|
||||
msgid "Episode {episodeNumber}, {episodeLabel}"
|
||||
msgstr "Episodio {episodeNumber}, {episodeLabel}"
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Episode not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/titles/season-accordion.tsx
|
||||
#: apps/native/src/hooks/use-title-actions.ts
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
@@ -1309,6 +1317,10 @@ msgstr "No se pudieron cargar los ajustes del programa de copias de seguridad"
|
||||
msgid "Failed to load title"
|
||||
msgstr "Error al cargar el título"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Failed to log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
#: apps/web/src/components/titles/use-title-actions.ts
|
||||
msgid "Failed to mark all episodes as watched"
|
||||
@@ -1365,6 +1377,11 @@ msgstr "Error al purgar la caché de metadatos"
|
||||
msgid "Failed to regenerate {label} URL"
|
||||
msgstr "Error al regenerar la URL de {label}"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Failed to remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
msgid "Failed to remove from library"
|
||||
msgstr "Error al eliminar de la biblioteca"
|
||||
@@ -1863,6 +1880,18 @@ msgstr "Cargando"
|
||||
msgid "Loading…"
|
||||
msgstr "Cargando…"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch…"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Login failed"
|
||||
msgstr "Error al iniciar sesión"
|
||||
@@ -2038,6 +2067,10 @@ msgstr "Lunes"
|
||||
msgid "More"
|
||||
msgstr "Más"
|
||||
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "More actions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
msgid "More Like This"
|
||||
msgstr "Más como este"
|
||||
@@ -2676,6 +2709,10 @@ msgstr "Recomendado"
|
||||
msgid "Recommended for You"
|
||||
msgstr "Recomendado para ti"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Record when you watched this."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Redirecting…"
|
||||
msgstr "Redirigiendo…"
|
||||
@@ -2739,6 +2776,11 @@ msgstr "Fecha de estreno"
|
||||
msgid "Remove"
|
||||
msgstr "Eliminar"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "Remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
@@ -2774,6 +2816,11 @@ msgstr "Eliminar foto"
|
||||
msgid "Remove profile picture"
|
||||
msgstr "Eliminar foto de perfil"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Removed from history"
|
||||
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
|
||||
@@ -3407,6 +3454,10 @@ msgstr "Este año"
|
||||
msgid "Thursday"
|
||||
msgstr "Jueves"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Time (optional)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Time:"
|
||||
msgstr "Hora:"
|
||||
@@ -3748,6 +3799,14 @@ msgstr "Marcar todo como visto"
|
||||
msgid "Watch history"
|
||||
msgstr "Historial de visionado"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Watch logged"
|
||||
msgstr ""
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Watch not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/title-availability.tsx
|
||||
#~ msgid "Watch on {name}"
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -962,6 +962,10 @@ msgstr "Sauvegardes de la base de données"
|
||||
msgid "Database restored. Reloading..."
|
||||
msgstr "Base de données restaurée. Rechargement..."
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/library/sort-menu.tsx
|
||||
#: apps/web/src/components/library/library-toolbar.tsx
|
||||
msgid "Date Added"
|
||||
@@ -1170,6 +1174,10 @@ msgstr "Épisode {episodeNumber}"
|
||||
msgid "Episode {episodeNumber}, {episodeLabel}"
|
||||
msgstr "Épisode {episodeNumber}, {episodeLabel}"
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Episode not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/titles/season-accordion.tsx
|
||||
#: apps/native/src/hooks/use-title-actions.ts
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
@@ -1309,6 +1317,10 @@ msgstr "Impossible de charger les paramètres de planification des sauvegardes"
|
||||
msgid "Failed to load title"
|
||||
msgstr "Échec du chargement du titre"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Failed to log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
#: apps/web/src/components/titles/use-title-actions.ts
|
||||
msgid "Failed to mark all episodes as watched"
|
||||
@@ -1365,6 +1377,11 @@ msgstr "Échec de la purge du cache de métadonnées"
|
||||
msgid "Failed to regenerate {label} URL"
|
||||
msgstr "Échec de la régénération de l'URL {label}"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Failed to remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
msgid "Failed to remove from library"
|
||||
msgstr "Échec de la suppression de la bibliothèque"
|
||||
@@ -1863,6 +1880,18 @@ msgstr "Chargement"
|
||||
msgid "Loading…"
|
||||
msgstr "Chargement…"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch…"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Login failed"
|
||||
msgstr "Connexion échouée"
|
||||
@@ -2038,6 +2067,10 @@ msgstr "Lundi"
|
||||
msgid "More"
|
||||
msgstr "Plus"
|
||||
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "More actions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
msgid "More Like This"
|
||||
msgstr "Dans le même genre"
|
||||
@@ -2676,6 +2709,10 @@ msgstr "Recommandé"
|
||||
msgid "Recommended for You"
|
||||
msgstr "Recommandé pour vous"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Record when you watched this."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Redirecting…"
|
||||
msgstr "Redirection…"
|
||||
@@ -2739,6 +2776,11 @@ msgstr "Date de sortie"
|
||||
msgid "Remove"
|
||||
msgstr "Supprimer"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "Remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
@@ -2774,6 +2816,11 @@ msgstr "Supprimer la photo"
|
||||
msgid "Remove profile picture"
|
||||
msgstr "Supprimer la photo de profil"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Removed from history"
|
||||
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
|
||||
@@ -3407,6 +3454,10 @@ msgstr "Cette année"
|
||||
msgid "Thursday"
|
||||
msgstr "Jeudi"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Time (optional)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Time:"
|
||||
msgstr "Heure :"
|
||||
@@ -3748,6 +3799,14 @@ msgstr "Tout marquer comme visionné"
|
||||
msgid "Watch history"
|
||||
msgstr "Historique de visionnage"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Watch logged"
|
||||
msgstr ""
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Watch not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/title-availability.tsx
|
||||
#~ msgid "Watch on {name}"
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -962,6 +962,10 @@ msgstr "גיבויי מסד נתונים"
|
||||
msgid "Database restored. Reloading..."
|
||||
msgstr "מסד הנתונים שוחזר. טוען מחדש..."
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/library/sort-menu.tsx
|
||||
#: apps/web/src/components/library/library-toolbar.tsx
|
||||
msgid "Date Added"
|
||||
@@ -1170,6 +1174,10 @@ msgstr "פרק {episodeNumber}"
|
||||
msgid "Episode {episodeNumber}, {episodeLabel}"
|
||||
msgstr "פרק {episodeNumber}, {episodeLabel}"
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Episode not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/titles/season-accordion.tsx
|
||||
#: apps/native/src/hooks/use-title-actions.ts
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
@@ -1309,6 +1317,10 @@ msgstr "טעינת הגדרות תזמון הגיבוי נכשלה"
|
||||
msgid "Failed to load title"
|
||||
msgstr "טעינת הכותרת נכשלה"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Failed to log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
#: apps/web/src/components/titles/use-title-actions.ts
|
||||
msgid "Failed to mark all episodes as watched"
|
||||
@@ -1365,6 +1377,11 @@ msgstr "ניקוי מטמון המטאדטה נכשל"
|
||||
msgid "Failed to regenerate {label} URL"
|
||||
msgstr "יצירת כתובת ה-URL של {label} מחדש נכשלה"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Failed to remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
msgid "Failed to remove from library"
|
||||
msgstr "הסרה מהספרייה נכשלה"
|
||||
@@ -1863,6 +1880,18 @@ msgstr "טוען"
|
||||
msgid "Loading…"
|
||||
msgstr "טוען…"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch…"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Login failed"
|
||||
msgstr "הכניסה נכשלה"
|
||||
@@ -2038,6 +2067,10 @@ msgstr "שני"
|
||||
msgid "More"
|
||||
msgstr "עוד"
|
||||
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "More actions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
msgid "More Like This"
|
||||
msgstr "עוד כמו זה"
|
||||
@@ -2676,6 +2709,10 @@ msgstr "מומלץ"
|
||||
msgid "Recommended for You"
|
||||
msgstr "מומלץ עבורך"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Record when you watched this."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Redirecting…"
|
||||
msgstr "מפנה…"
|
||||
@@ -2739,6 +2776,11 @@ msgstr "תאריך יציאה"
|
||||
msgid "Remove"
|
||||
msgstr "הסר"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "Remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
@@ -2774,6 +2816,11 @@ msgstr "הסר תמונה"
|
||||
msgid "Remove profile picture"
|
||||
msgstr "הסר תמונת פרופיל"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Removed from history"
|
||||
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
|
||||
@@ -3407,6 +3454,10 @@ msgstr "השנה"
|
||||
msgid "Thursday"
|
||||
msgstr "חמישי"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Time (optional)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Time:"
|
||||
msgstr "שעה:"
|
||||
@@ -3748,6 +3799,14 @@ msgstr "סמן הכל כנצפה"
|
||||
msgid "Watch history"
|
||||
msgstr "היסטוריית צפייה"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Watch logged"
|
||||
msgstr ""
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Watch not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/title-availability.tsx
|
||||
#~ msgid "Watch on {name}"
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -962,6 +962,10 @@ msgstr "Backup del database"
|
||||
msgid "Database restored. Reloading..."
|
||||
msgstr "Database ripristinato. Ricaricamento..."
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/library/sort-menu.tsx
|
||||
#: apps/web/src/components/library/library-toolbar.tsx
|
||||
msgid "Date Added"
|
||||
@@ -1170,6 +1174,10 @@ msgstr "Episodio {episodeNumber}"
|
||||
msgid "Episode {episodeNumber}, {episodeLabel}"
|
||||
msgstr "Episodio {episodeNumber}, {episodeLabel}"
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Episode not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/titles/season-accordion.tsx
|
||||
#: apps/native/src/hooks/use-title-actions.ts
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
@@ -1309,6 +1317,10 @@ msgstr "Impossibile caricare le impostazioni di pianificazione del backup"
|
||||
msgid "Failed to load title"
|
||||
msgstr "Impossibile caricare il titolo"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Failed to log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
#: apps/web/src/components/titles/use-title-actions.ts
|
||||
msgid "Failed to mark all episodes as watched"
|
||||
@@ -1365,6 +1377,11 @@ msgstr "Impossibile svuotare la cache metadati"
|
||||
msgid "Failed to regenerate {label} URL"
|
||||
msgstr "Impossibile rigenerare l'URL di {label}"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Failed to remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
msgid "Failed to remove from library"
|
||||
msgstr "Impossibile rimuovere dalla libreria"
|
||||
@@ -1863,6 +1880,18 @@ msgstr "Caricamento"
|
||||
msgid "Loading…"
|
||||
msgstr "Caricamento…"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch…"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Login failed"
|
||||
msgstr "Accesso non riuscito"
|
||||
@@ -2038,6 +2067,10 @@ msgstr "Lunedì"
|
||||
msgid "More"
|
||||
msgstr "Altro"
|
||||
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "More actions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
msgid "More Like This"
|
||||
msgstr "Simili a questo"
|
||||
@@ -2676,6 +2709,10 @@ msgstr "Consigliati"
|
||||
msgid "Recommended for You"
|
||||
msgstr "Consigliati per te"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Record when you watched this."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Redirecting…"
|
||||
msgstr "Reindirizzamento in corso…"
|
||||
@@ -2739,6 +2776,11 @@ msgstr "Data di uscita"
|
||||
msgid "Remove"
|
||||
msgstr "Rimuovi"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "Remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
@@ -2774,6 +2816,11 @@ msgstr "Rimuovi immagine"
|
||||
msgid "Remove profile picture"
|
||||
msgstr "Rimuovi immagine del profilo"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Removed from history"
|
||||
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
|
||||
@@ -3407,6 +3454,10 @@ msgstr "Quest'anno"
|
||||
msgid "Thursday"
|
||||
msgstr "Giovedì"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Time (optional)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Time:"
|
||||
msgstr "Ora:"
|
||||
@@ -3748,6 +3799,14 @@ msgstr "Segna tutta la stagione come vista"
|
||||
msgid "Watch history"
|
||||
msgstr "Cronologia visioni"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Watch logged"
|
||||
msgstr ""
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Watch not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/title-availability.tsx
|
||||
#~ msgid "Watch on {name}"
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -962,6 +962,10 @@ msgstr "データベースバックアップ"
|
||||
msgid "Database restored. Reloading..."
|
||||
msgstr "データベースを復元しました。再読み込み中..."
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/library/sort-menu.tsx
|
||||
#: apps/web/src/components/library/library-toolbar.tsx
|
||||
msgid "Date Added"
|
||||
@@ -1170,6 +1174,10 @@ msgstr "エピソード{episodeNumber}"
|
||||
msgid "Episode {episodeNumber}, {episodeLabel}"
|
||||
msgstr "エピソード{episodeNumber}、{episodeLabel}"
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Episode not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/titles/season-accordion.tsx
|
||||
#: apps/native/src/hooks/use-title-actions.ts
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
@@ -1309,6 +1317,10 @@ msgstr "バックアップスケジュール設定を読み込めませんでし
|
||||
msgid "Failed to load title"
|
||||
msgstr "作品の読み込みに失敗しました"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Failed to log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
#: apps/web/src/components/titles/use-title-actions.ts
|
||||
msgid "Failed to mark all episodes as watched"
|
||||
@@ -1365,6 +1377,11 @@ msgstr "メタデータキャッシュのクリアに失敗しました"
|
||||
msgid "Failed to regenerate {label} URL"
|
||||
msgstr "{label} URLの再生成に失敗しました"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Failed to remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
msgid "Failed to remove from library"
|
||||
msgstr "ライブラリからの削除に失敗しました"
|
||||
@@ -1863,6 +1880,18 @@ msgstr "読み込み中"
|
||||
msgid "Loading…"
|
||||
msgstr "読み込み中…"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch…"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Login failed"
|
||||
msgstr "ログインに失敗しました"
|
||||
@@ -2038,6 +2067,10 @@ msgstr "月曜日"
|
||||
msgid "More"
|
||||
msgstr "もっと見る"
|
||||
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "More actions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
msgid "More Like This"
|
||||
msgstr "関連作品"
|
||||
@@ -2676,6 +2709,10 @@ msgstr "おすすめ作品"
|
||||
msgid "Recommended for You"
|
||||
msgstr "あなたへのおすすめ"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Record when you watched this."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Redirecting…"
|
||||
msgstr "リダイレクト中…"
|
||||
@@ -2739,6 +2776,11 @@ msgstr "リリース日"
|
||||
msgid "Remove"
|
||||
msgstr "削除"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "Remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
@@ -2774,6 +2816,11 @@ msgstr "画像を削除"
|
||||
msgid "Remove profile picture"
|
||||
msgstr "プロフィール画像を削除する"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Removed from history"
|
||||
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
|
||||
@@ -3407,6 +3454,10 @@ msgstr "今年"
|
||||
msgid "Thursday"
|
||||
msgstr "木曜日"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Time (optional)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Time:"
|
||||
msgstr "時刻:"
|
||||
@@ -3748,6 +3799,14 @@ msgstr "すべて視聴済みにする"
|
||||
msgid "Watch history"
|
||||
msgstr "視聴履歴"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Watch logged"
|
||||
msgstr ""
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Watch not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/title-availability.tsx
|
||||
#~ msgid "Watch on {name}"
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -962,6 +962,10 @@ msgstr "데이터베이스 백업"
|
||||
msgid "Database restored. Reloading..."
|
||||
msgstr "데이터베이스가 복원되었습니다. 다시 로드 중..."
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/library/sort-menu.tsx
|
||||
#: apps/web/src/components/library/library-toolbar.tsx
|
||||
msgid "Date Added"
|
||||
@@ -1170,6 +1174,10 @@ msgstr "에피소드 {episodeNumber}"
|
||||
msgid "Episode {episodeNumber}, {episodeLabel}"
|
||||
msgstr "에피소드 {episodeNumber}, {episodeLabel}"
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Episode not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/titles/season-accordion.tsx
|
||||
#: apps/native/src/hooks/use-title-actions.ts
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
@@ -1309,6 +1317,10 @@ msgstr "백업 일정 설정을 불러오지 못했습니다"
|
||||
msgid "Failed to load title"
|
||||
msgstr "작품 정보를 불러오지 못했습니다"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Failed to log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
#: apps/web/src/components/titles/use-title-actions.ts
|
||||
msgid "Failed to mark all episodes as watched"
|
||||
@@ -1365,6 +1377,11 @@ msgstr "메타데이터 캐시 삭제 실패"
|
||||
msgid "Failed to regenerate {label} URL"
|
||||
msgstr "{label} URL 재생성 실패"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Failed to remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
msgid "Failed to remove from library"
|
||||
msgstr "라이브러리에서 제거 실패"
|
||||
@@ -1863,6 +1880,18 @@ msgstr "로딩 중"
|
||||
msgid "Loading…"
|
||||
msgstr "로딩 중…"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch…"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Login failed"
|
||||
msgstr "로그인 실패"
|
||||
@@ -2038,6 +2067,10 @@ msgstr "월요일"
|
||||
msgid "More"
|
||||
msgstr "더 보기"
|
||||
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "More actions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
msgid "More Like This"
|
||||
msgstr "비슷한 작품"
|
||||
@@ -2676,6 +2709,10 @@ msgstr "추천 작품"
|
||||
msgid "Recommended for You"
|
||||
msgstr "맞춤 추천"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Record when you watched this."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Redirecting…"
|
||||
msgstr "리디렉션 중…"
|
||||
@@ -2739,6 +2776,11 @@ msgstr "출시일"
|
||||
msgid "Remove"
|
||||
msgstr "제거"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "Remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
@@ -2774,6 +2816,11 @@ msgstr "사진 제거"
|
||||
msgid "Remove profile picture"
|
||||
msgstr "프로필 사진 제거"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Removed from history"
|
||||
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
|
||||
@@ -3407,6 +3454,10 @@ msgstr "올해"
|
||||
msgid "Thursday"
|
||||
msgstr "목요일"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Time (optional)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Time:"
|
||||
msgstr "시간:"
|
||||
@@ -3748,6 +3799,14 @@ msgstr "모두 시청 완료"
|
||||
msgid "Watch history"
|
||||
msgstr "시청 기록"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Watch logged"
|
||||
msgstr ""
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Watch not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/title-availability.tsx
|
||||
#~ msgid "Watch on {name}"
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -962,6 +962,10 @@ msgstr "Database back-ups"
|
||||
msgid "Database restored. Reloading..."
|
||||
msgstr "Database hersteld. Opnieuw laden..."
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/library/sort-menu.tsx
|
||||
#: apps/web/src/components/library/library-toolbar.tsx
|
||||
msgid "Date Added"
|
||||
@@ -1170,6 +1174,10 @@ msgstr "Aflevering {episodeNumber}"
|
||||
msgid "Episode {episodeNumber}, {episodeLabel}"
|
||||
msgstr "Aflevering {episodeNumber}, {episodeLabel}"
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Episode not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/titles/season-accordion.tsx
|
||||
#: apps/native/src/hooks/use-title-actions.ts
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
@@ -1309,6 +1317,10 @@ msgstr "Kon instellingen voor back-upplanning niet laden"
|
||||
msgid "Failed to load title"
|
||||
msgstr "Titel laden mislukt"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Failed to log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
#: apps/web/src/components/titles/use-title-actions.ts
|
||||
msgid "Failed to mark all episodes as watched"
|
||||
@@ -1365,6 +1377,11 @@ msgstr "Metadatacache leegmaken mislukt"
|
||||
msgid "Failed to regenerate {label} URL"
|
||||
msgstr "{label} URL opnieuw genereren mislukt"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Failed to remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
msgid "Failed to remove from library"
|
||||
msgstr "Verwijderen uit bibliotheek mislukt"
|
||||
@@ -1863,6 +1880,18 @@ msgstr "Laden"
|
||||
msgid "Loading…"
|
||||
msgstr "Laden…"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch…"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Login failed"
|
||||
msgstr "Aanmelden mislukt"
|
||||
@@ -2038,6 +2067,10 @@ msgstr "Maandag"
|
||||
msgid "More"
|
||||
msgstr "Meer"
|
||||
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "More actions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
msgid "More Like This"
|
||||
msgstr "Meer zoals dit"
|
||||
@@ -2676,6 +2709,10 @@ msgstr "Aanbevolen"
|
||||
msgid "Recommended for You"
|
||||
msgstr "Aanbevolen voor jou"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Record when you watched this."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Redirecting…"
|
||||
msgstr "Doorsturen…"
|
||||
@@ -2739,6 +2776,11 @@ msgstr "Releasedatum"
|
||||
msgid "Remove"
|
||||
msgstr "Verwijderen"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "Remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
@@ -2774,6 +2816,11 @@ msgstr "Afbeelding verwijderen"
|
||||
msgid "Remove profile picture"
|
||||
msgstr "Profielfoto verwijderen"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Removed from history"
|
||||
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
|
||||
@@ -3407,6 +3454,10 @@ msgstr "Dit jaar"
|
||||
msgid "Thursday"
|
||||
msgstr "Donderdag"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Time (optional)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Time:"
|
||||
msgstr "Tijd:"
|
||||
@@ -3748,6 +3799,14 @@ msgstr "Alles als bekeken markeren"
|
||||
msgid "Watch history"
|
||||
msgstr "Kijkgeschiedenis"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Watch logged"
|
||||
msgstr ""
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Watch not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/title-availability.tsx
|
||||
#~ msgid "Watch on {name}"
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -962,6 +962,10 @@ msgstr "Backups do banco de dados"
|
||||
msgid "Database restored. Reloading..."
|
||||
msgstr "Banco de dados restaurado. Recarregando..."
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/library/sort-menu.tsx
|
||||
#: apps/web/src/components/library/library-toolbar.tsx
|
||||
msgid "Date Added"
|
||||
@@ -1170,6 +1174,10 @@ msgstr "Episódio {episodeNumber}"
|
||||
msgid "Episode {episodeNumber}, {episodeLabel}"
|
||||
msgstr "Episódio {episodeNumber}, {episodeLabel}"
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Episode not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/titles/season-accordion.tsx
|
||||
#: apps/native/src/hooks/use-title-actions.ts
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
@@ -1309,6 +1317,10 @@ msgstr "Falha ao carregar as definições de agendamento de backup"
|
||||
msgid "Failed to load title"
|
||||
msgstr "Falha ao carregar título"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Failed to log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
#: apps/web/src/components/titles/use-title-actions.ts
|
||||
msgid "Failed to mark all episodes as watched"
|
||||
@@ -1365,6 +1377,11 @@ msgstr "Falha ao limpar cache de metadados"
|
||||
msgid "Failed to regenerate {label} URL"
|
||||
msgstr "Falha ao regenerar URL de {label}"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Failed to remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
msgid "Failed to remove from library"
|
||||
msgstr "Falha ao remover da biblioteca"
|
||||
@@ -1863,6 +1880,18 @@ msgstr "A carregar"
|
||||
msgid "Loading…"
|
||||
msgstr "Carregando…"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch…"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Login failed"
|
||||
msgstr "Falha no login"
|
||||
@@ -2038,6 +2067,10 @@ msgstr "Segunda-feira"
|
||||
msgid "More"
|
||||
msgstr "Mais"
|
||||
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "More actions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
msgid "More Like This"
|
||||
msgstr "Mais Como Este"
|
||||
@@ -2676,6 +2709,10 @@ msgstr "Recomendado"
|
||||
msgid "Recommended for You"
|
||||
msgstr "Recomendado para Você"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Record when you watched this."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Redirecting…"
|
||||
msgstr "Redirecionando…"
|
||||
@@ -2739,6 +2776,11 @@ msgstr "Data de Lançamento"
|
||||
msgid "Remove"
|
||||
msgstr "Remover"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "Remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
@@ -2774,6 +2816,11 @@ msgstr "Remover foto"
|
||||
msgid "Remove profile picture"
|
||||
msgstr "Remover foto de perfil"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Removed from history"
|
||||
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
|
||||
@@ -3407,6 +3454,10 @@ msgstr "Este Ano"
|
||||
msgid "Thursday"
|
||||
msgstr "Quinta-feira"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Time (optional)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Time:"
|
||||
msgstr "Hora:"
|
||||
@@ -3748,6 +3799,14 @@ msgstr "Marcar todos"
|
||||
msgid "Watch history"
|
||||
msgstr "Histórico de exibição"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Watch logged"
|
||||
msgstr ""
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Watch not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/title-availability.tsx
|
||||
#~ msgid "Watch on {name}"
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -962,6 +962,10 @@ msgstr "数据库备份"
|
||||
msgid "Database restored. Reloading..."
|
||||
msgstr "数据库已还原。正在重新加载..."
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/library/sort-menu.tsx
|
||||
#: apps/web/src/components/library/library-toolbar.tsx
|
||||
msgid "Date Added"
|
||||
@@ -1170,6 +1174,10 @@ msgstr "第 {episodeNumber} 集"
|
||||
msgid "Episode {episodeNumber}, {episodeLabel}"
|
||||
msgstr "第 {episodeNumber} 集,{episodeLabel}"
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Episode not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/titles/season-accordion.tsx
|
||||
#: apps/native/src/hooks/use-title-actions.ts
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
@@ -1309,6 +1317,10 @@ msgstr "加载备份计划设置失败"
|
||||
msgid "Failed to load title"
|
||||
msgstr "加载影视内容失败"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Failed to log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
#: apps/web/src/components/titles/use-title-actions.ts
|
||||
msgid "Failed to mark all episodes as watched"
|
||||
@@ -1365,6 +1377,11 @@ msgstr "清除元数据缓存失败"
|
||||
msgid "Failed to regenerate {label} URL"
|
||||
msgstr "重新生成 {label} URL 失败"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Failed to remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/lib/title-actions.ts
|
||||
msgid "Failed to remove from library"
|
||||
msgstr "从媒体库移除失败"
|
||||
@@ -1863,6 +1880,18 @@ msgstr "加载中"
|
||||
msgid "Loading…"
|
||||
msgstr "加载中…"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log a watch…"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Log watch"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Login failed"
|
||||
msgstr "登录失败"
|
||||
@@ -2038,6 +2067,10 @@ msgstr "星期一"
|
||||
msgid "More"
|
||||
msgstr "更多"
|
||||
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "More actions"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/app/title/[id].tsx
|
||||
msgid "More Like This"
|
||||
msgstr "更多相似内容"
|
||||
@@ -2676,6 +2709,10 @@ msgstr "推荐内容"
|
||||
msgid "Recommended for You"
|
||||
msgstr "为您推荐"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Record when you watched this."
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/auth-form.tsx
|
||||
msgid "Redirecting…"
|
||||
msgstr "正在跳转…"
|
||||
@@ -2739,6 +2776,11 @@ msgstr "发行日期"
|
||||
msgid "Remove"
|
||||
msgstr "移除"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/history-row.tsx
|
||||
msgid "Remove from history"
|
||||
msgstr ""
|
||||
|
||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||
#: apps/native/src/components/titles/status-action-button.tsx
|
||||
@@ -2774,6 +2816,11 @@ msgstr "删除图片"
|
||||
msgid "Remove profile picture"
|
||||
msgstr "删除头像"
|
||||
|
||||
#: apps/native/src/components/history/history-row.tsx
|
||||
#: apps/web/src/components/history/use-delete-watch.ts
|
||||
msgid "Removed from history"
|
||||
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
|
||||
@@ -3407,6 +3454,10 @@ msgstr "今年"
|
||||
msgid "Thursday"
|
||||
msgstr "星期四"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Time (optional)"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||
msgid "Time:"
|
||||
msgstr "时间:"
|
||||
@@ -3748,6 +3799,14 @@ msgstr "标记全部为已看"
|
||||
msgid "Watch history"
|
||||
msgstr "观看历史"
|
||||
|
||||
#: apps/web/src/components/titles/log-watch-dialog.tsx
|
||||
msgid "Watch logged"
|
||||
msgstr ""
|
||||
|
||||
#: packages/i18n/src/errors.ts
|
||||
msgid "Watch not found"
|
||||
msgstr ""
|
||||
|
||||
#: apps/web/src/components/titles/title-availability.tsx
|
||||
#~ msgid "Watch on {name}"
|
||||
#~ msgstr ""
|
||||
|
||||
Reference in new issue
Block a user