mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
feat: add upcoming episodes and movies timeline (#17)
This commit is contained in:
@@ -18,6 +18,7 @@ import { useCSSVariable } from "uniwind";
|
|||||||
import { ContinueWatchingCard } from "@/components/dashboard/continue-watching-card";
|
import { ContinueWatchingCard } from "@/components/dashboard/continue-watching-card";
|
||||||
import { HorizontalPosterRow } from "@/components/dashboard/horizontal-poster-row";
|
import { HorizontalPosterRow } from "@/components/dashboard/horizontal-poster-row";
|
||||||
import { StatsCard } from "@/components/dashboard/stats-card";
|
import { StatsCard } from "@/components/dashboard/stats-card";
|
||||||
|
import { UpcomingSection } from "@/components/dashboard/upcoming-section";
|
||||||
import { EmptyState } from "@/components/ui/empty-state";
|
import { EmptyState } from "@/components/ui/empty-state";
|
||||||
import {
|
import {
|
||||||
HorizontalListSeparator,
|
HorizontalListSeparator,
|
||||||
@@ -179,8 +180,13 @@ export default function DashboardScreen() {
|
|||||||
</Animated.View>
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Upcoming */}
|
||||||
|
<Animated.View entering={FadeInDown.duration(300).delay(250)}>
|
||||||
|
<UpcomingSection />
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
{/* Library */}
|
{/* Library */}
|
||||||
<Animated.View entering={FadeInDown.duration(300).delay(300)}>
|
<Animated.View entering={FadeInDown.duration(300).delay(350)}>
|
||||||
<View className="px-4">
|
<View className="px-4">
|
||||||
<SectionHeader title={t`In Your Library`} icon={IconBooks} />
|
<SectionHeader title={t`In Your Library`} icon={IconBooks} />
|
||||||
</View>
|
</View>
|
||||||
@@ -200,7 +206,7 @@ export default function DashboardScreen() {
|
|||||||
|
|
||||||
{/* Recommendations */}
|
{/* Recommendations */}
|
||||||
{hasRecommendations && (
|
{hasRecommendations && (
|
||||||
<Animated.View entering={FadeInDown.duration(300).delay(400)}>
|
<Animated.View entering={FadeInDown.duration(300).delay(450)}>
|
||||||
<View className="px-4">
|
<View className="px-4">
|
||||||
<SectionHeader title={t`Recommended for You`} icon={IconThumbUp} />
|
<SectionHeader title={t`Recommended for You`} icon={IconThumbUp} />
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
|
import { IconCalendarEvent } from "@tabler/icons-react-native";
|
||||||
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
|
import { Stack } from "expo-router";
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { RefreshControl, SectionList, View } from "react-native";
|
||||||
|
import Animated, { FadeInDown } from "react-native-reanimated";
|
||||||
|
import { useCSSVariable, useResolveClassNames } from "uniwind";
|
||||||
|
|
||||||
|
import { UpcomingRow } from "@/components/dashboard/upcoming-row";
|
||||||
|
import { ScaledIcon } from "@/components/ui/scaled-icon";
|
||||||
|
import { Text } from "@/components/ui/text";
|
||||||
|
import { orpc } from "@/lib/orpc";
|
||||||
|
import { queryClient } from "@/lib/query-client";
|
||||||
|
import { groupByDateBucket } from "@sofa/i18n/date-buckets";
|
||||||
|
|
||||||
|
const contentContainerStyle = { paddingBottom: 24 };
|
||||||
|
|
||||||
|
export default function UpcomingScreen() {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const headerTitleStyle = useResolveClassNames("font-display text-foreground text-xl");
|
||||||
|
const tintColor = useCSSVariable("--color-primary") as string;
|
||||||
|
const backgroundColor = useCSSVariable("--color-background") as string;
|
||||||
|
const mutedColor = useCSSVariable("--color-muted-foreground") as string;
|
||||||
|
|
||||||
|
const { data, isPending, fetchNextPage, hasNextPage, isFetchingNextPage, isRefetching } =
|
||||||
|
useInfiniteQuery(
|
||||||
|
orpc.dashboard.upcoming.infiniteOptions({
|
||||||
|
input: (pageParam: string | undefined) => ({
|
||||||
|
days: 90,
|
||||||
|
limit: 20,
|
||||||
|
cursor: pageParam,
|
||||||
|
}),
|
||||||
|
initialPageParam: undefined as string | undefined,
|
||||||
|
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const allItems = useMemo(() => data?.pages.flatMap((p) => p.items) ?? [], [data]);
|
||||||
|
const sections = useMemo(
|
||||||
|
() =>
|
||||||
|
groupByDateBucket(allItems).map((b) => ({
|
||||||
|
key: b.key,
|
||||||
|
title: b.label,
|
||||||
|
data: b.items,
|
||||||
|
})),
|
||||||
|
[allItems],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onRefresh = useCallback(() => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: orpc.dashboard.upcoming.key() });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onEndReached = useCallback(() => {
|
||||||
|
if (hasNextPage && !isFetchingNextPage) {
|
||||||
|
fetchNextPage();
|
||||||
|
}
|
||||||
|
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Header
|
||||||
|
transparent
|
||||||
|
blurEffect="systemChromeMaterialDark"
|
||||||
|
style={{
|
||||||
|
color: tintColor,
|
||||||
|
shadowColor: "transparent",
|
||||||
|
backgroundColor: process.env.EXPO_OS === "ios" ? undefined : backgroundColor,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Stack.Screen.Title style={headerTitleStyle as Record<string, unknown>}>
|
||||||
|
{t`Upcoming`}
|
||||||
|
</Stack.Screen.Title>
|
||||||
|
<Stack.Screen.BackButton displayMode="minimal" />
|
||||||
|
{!isPending && allItems.length === 0 ? (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInDown.duration(300)}
|
||||||
|
className="flex-1 items-center justify-center px-4"
|
||||||
|
>
|
||||||
|
<ScaledIcon icon={IconCalendarEvent} size={48} color={`${mutedColor}66`} />
|
||||||
|
<Text className="text-muted-foreground mt-4 text-center text-sm">
|
||||||
|
<Trans>No upcoming episodes or releases in the next 90 days.</Trans>
|
||||||
|
</Text>
|
||||||
|
</Animated.View>
|
||||||
|
) : (
|
||||||
|
<SectionList
|
||||||
|
sections={sections}
|
||||||
|
keyExtractor={(item, i) => `${item.titleId}-${item.date}-${i}`}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<View className="px-4 py-1">
|
||||||
|
<UpcomingRow item={item} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
renderSectionHeader={({ section: { title } }) => (
|
||||||
|
<View className="bg-background px-4 pt-4 pb-1">
|
||||||
|
<Text className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
stickySectionHeadersEnabled
|
||||||
|
contentContainerStyle={contentContainerStyle}
|
||||||
|
contentInsetAdjustmentBehavior="automatic"
|
||||||
|
onEndReached={onEndReached}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
refreshControl={<RefreshControl refreshing={isRefetching} onRefresh={onRefresh} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { plural } from "@lingui/core/macro";
|
||||||
|
import { useLingui } from "@lingui/react/macro";
|
||||||
|
import { IconMovie } from "@tabler/icons-react-native";
|
||||||
|
import { Link } from "expo-router";
|
||||||
|
import { Pressable, View } from "react-native";
|
||||||
|
import { useCSSVariable } from "uniwind";
|
||||||
|
|
||||||
|
import { Image } from "@/components/ui/image";
|
||||||
|
import { ScaledIcon } from "@/components/ui/scaled-icon";
|
||||||
|
import { Text } from "@/components/ui/text";
|
||||||
|
import { titleActions } from "@/lib/title-actions";
|
||||||
|
import type { UpcomingItem } from "@sofa/api/schemas";
|
||||||
|
|
||||||
|
const statusColors = {
|
||||||
|
in_watchlist: "--color-status-watchlist",
|
||||||
|
watching: "--color-status-watching",
|
||||||
|
caught_up: "--color-status-completed",
|
||||||
|
completed: "--color-status-completed",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
function formatShortDate(dateStr: string): string {
|
||||||
|
const d = new Date(`${dateStr}T00:00:00Z`);
|
||||||
|
return new Intl.DateTimeFormat(undefined, {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
timeZone: "UTC",
|
||||||
|
}).format(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UpcomingRow({ item }: { item: UpcomingItem }) {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const statusColor = useCSSVariable(statusColors[item.userStatus]) as string;
|
||||||
|
const mutedColor = useCSSVariable("--color-muted-foreground") as string;
|
||||||
|
|
||||||
|
let subtitle: string;
|
||||||
|
if (item.titleType === "movie") {
|
||||||
|
subtitle = formatShortDate(item.date);
|
||||||
|
} else if (item.episodeCount > 1 && item.seasonNumber != null) {
|
||||||
|
const seasonNum = item.seasonNumber;
|
||||||
|
const epCount = item.episodeCount;
|
||||||
|
subtitle = t`S${seasonNum} \u00b7 ${plural(epCount, { one: "# episode", other: "# episodes" })}`;
|
||||||
|
} else {
|
||||||
|
const episodeLabel =
|
||||||
|
item.seasonNumber != null && item.episodeNumber != null
|
||||||
|
? `S${item.seasonNumber} E${item.episodeNumber}`
|
||||||
|
: null;
|
||||||
|
subtitle = [episodeLabel, item.episodeName].filter(Boolean).join(" \u00b7 ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowContent = (
|
||||||
|
<Pressable className="bg-card/40 flex-row items-center gap-3 rounded-xl border border-white/[0.06] px-3 py-3">
|
||||||
|
{/* Poster */}
|
||||||
|
<View className="overflow-hidden rounded-lg" style={{ width: 44, height: 66 }}>
|
||||||
|
{item.posterPath ? (
|
||||||
|
<Image
|
||||||
|
source={{ uri: item.posterPath }}
|
||||||
|
thumbHash={item.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>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<View className="min-w-0 flex-1">
|
||||||
|
<View className="flex-row items-center gap-2">
|
||||||
|
{/* Status dot with halo */}
|
||||||
|
<View className="relative" style={{ width: 8, height: 8 }}>
|
||||||
|
<View
|
||||||
|
className="absolute inset-0 rounded-full opacity-40"
|
||||||
|
style={{ backgroundColor: statusColor }}
|
||||||
|
/>
|
||||||
|
<View className="size-2 rounded-full" style={{ backgroundColor: statusColor }} />
|
||||||
|
</View>
|
||||||
|
<Text className="text-foreground flex-shrink text-sm font-medium" numberOfLines={1}>
|
||||||
|
{item.titleName}
|
||||||
|
</Text>
|
||||||
|
{item.isNewSeason && (
|
||||||
|
<View className="bg-primary/15 rounded px-1.5 py-0.5">
|
||||||
|
<Text className="text-primary text-[10px] font-semibold tracking-wider uppercase">
|
||||||
|
{t`New Season`}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View className="mt-1 flex-row items-center gap-1">
|
||||||
|
{item.titleType === "movie" && (
|
||||||
|
<ScaledIcon icon={IconMovie} size={12} color={mutedColor} />
|
||||||
|
)}
|
||||||
|
<Text className="text-muted-foreground flex-shrink text-xs" numberOfLines={1}>
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Right column: date + provider logo */}
|
||||||
|
<View className="items-end gap-1">
|
||||||
|
<Text className="text-muted-foreground text-xs">{formatShortDate(item.date)}</Text>
|
||||||
|
{item.streamingProvider &&
|
||||||
|
(item.streamingProvider.logoPath ? (
|
||||||
|
<View
|
||||||
|
className="overflow-hidden rounded-lg border border-white/[0.06]"
|
||||||
|
style={{ width: 28, height: 28 }}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={{ uri: item.streamingProvider.logoPath }}
|
||||||
|
className="size-full"
|
||||||
|
contentFit="cover"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Text className="text-muted-foreground/60 text-[10px]">
|
||||||
|
{item.streamingProvider.providerName}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={`/title/${item.titleId}`} asChild>
|
||||||
|
<Link.Trigger withAppleZoom>{rowContent}</Link.Trigger>
|
||||||
|
<Link.Preview />
|
||||||
|
<Link.Menu>
|
||||||
|
{item.titleType === "movie" && (
|
||||||
|
<Link.MenuAction
|
||||||
|
title={t`Mark as Watched`}
|
||||||
|
icon="checkmark.circle"
|
||||||
|
onPress={() => titleActions.markMovieWatched(item.titleId, item.titleName)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.titleType === "tv" && item.episodeId && (
|
||||||
|
<Link.MenuAction
|
||||||
|
title={t`Mark Episode Watched`}
|
||||||
|
icon="checkmark.circle"
|
||||||
|
onPress={() => titleActions.watchEpisode(item.episodeId!)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Link.MenuAction
|
||||||
|
title={t`Remove from Library`}
|
||||||
|
icon="trash"
|
||||||
|
destructive
|
||||||
|
onPress={() => titleActions.removeFromLibrary(item.titleId)}
|
||||||
|
/>
|
||||||
|
</Link.Menu>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { useLingui } from "@lingui/react/macro";
|
||||||
|
import { IconCalendarEvent } from "@tabler/icons-react-native";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
import { SectionHeader } from "@/components/ui/section-header";
|
||||||
|
import { orpc } from "@/lib/orpc";
|
||||||
|
|
||||||
|
import { UpcomingRow } from "./upcoming-row";
|
||||||
|
|
||||||
|
export function UpcomingSection() {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const { push } = useRouter();
|
||||||
|
const { data, isPending } = useQuery(
|
||||||
|
orpc.dashboard.upcoming.queryOptions({ input: { days: 7, limit: 5 } }),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isPending) return null;
|
||||||
|
|
||||||
|
const items = data?.items ?? [];
|
||||||
|
if (items.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<View className="px-4">
|
||||||
|
<SectionHeader
|
||||||
|
title={t`Upcoming`}
|
||||||
|
icon={IconCalendarEvent}
|
||||||
|
onSeeAll={() => push("/upcoming")}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View className="gap-2 px-4">
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<UpcomingRow key={`${item.titleId}-${item.date}-${i}`} item={item} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { useLingui } from "@lingui/react/macro";
|
||||||
import type { Icon } from "@tabler/icons-react-native";
|
import type { Icon } from "@tabler/icons-react-native";
|
||||||
import { View } from "react-native";
|
import { Pressable, View } from "react-native";
|
||||||
import { useCSSVariable } from "uniwind";
|
import { useCSSVariable } from "uniwind";
|
||||||
|
|
||||||
import { ScaledIcon } from "@/components/ui/scaled-icon";
|
import { ScaledIcon } from "@/components/ui/scaled-icon";
|
||||||
@@ -9,16 +10,30 @@ interface SectionHeaderProps {
|
|||||||
title: string;
|
title: string;
|
||||||
icon?: Icon;
|
icon?: Icon;
|
||||||
iconColor?: string;
|
iconColor?: string;
|
||||||
|
onSeeAll?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SectionHeader({ title, icon: IconComponent, iconColor }: SectionHeaderProps) {
|
export function SectionHeader({
|
||||||
|
title,
|
||||||
|
icon: IconComponent,
|
||||||
|
iconColor,
|
||||||
|
onSeeAll,
|
||||||
|
}: SectionHeaderProps) {
|
||||||
|
const { t } = useLingui();
|
||||||
const primaryColor = useCSSVariable("--color-primary") as string;
|
const primaryColor = useCSSVariable("--color-primary") as string;
|
||||||
const resolvedColor = iconColor ?? primaryColor;
|
const resolvedColor = iconColor ?? primaryColor;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="mb-3 flex-row items-center gap-2">
|
<View className="mb-3 flex-row items-center justify-between">
|
||||||
{IconComponent && <ScaledIcon icon={IconComponent} size={20} color={resolvedColor} />}
|
<View className="flex-row items-center gap-2">
|
||||||
<Text className="font-display text-foreground text-xl tracking-tight">{title}</Text>
|
{IconComponent && <ScaledIcon icon={IconComponent} size={20} color={resolvedColor} />}
|
||||||
|
<Text className="font-display text-foreground text-xl tracking-tight">{title}</Text>
|
||||||
|
</View>
|
||||||
|
{onSeeAll && (
|
||||||
|
<Pressable onPress={onSeeAll} hitSlop={8}>
|
||||||
|
<Text className="text-primary text-sm">{t`See all`}</Text>
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
getContinueWatchingFeed,
|
getContinueWatchingFeed,
|
||||||
getLibraryFeed,
|
getLibraryFeed,
|
||||||
getRecommendationsFeed,
|
getRecommendationsFeed,
|
||||||
|
getUpcomingFeed,
|
||||||
getUserStats,
|
getUserStats,
|
||||||
getWatchCount,
|
getWatchCount,
|
||||||
getWatchHistory,
|
getWatchHistory,
|
||||||
@@ -87,6 +88,27 @@ export const recommendations = os.dashboard.recommendations.use(authed).handler(
|
|||||||
return { items };
|
return { items };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const upcoming = os.dashboard.upcoming.use(authed).handler(({ input, context }) => {
|
||||||
|
const result = getUpcomingFeed(context.user.id, {
|
||||||
|
days: input.days,
|
||||||
|
limit: input.limit,
|
||||||
|
cursor: input.cursor,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
items: result.items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
posterPath: tmdbImageUrl(item.posterPath, "posters"),
|
||||||
|
streamingProvider: item.streamingProvider
|
||||||
|
? {
|
||||||
|
...item.streamingProvider,
|
||||||
|
logoPath: tmdbImageUrl(item.streamingProvider.logoPath, "logos"),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
})),
|
||||||
|
nextCursor: result.nextCursor,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
export const watchHistory = os.dashboard.watchHistory.use(authed).handler(({ input, context }) => {
|
export const watchHistory = os.dashboard.watchHistory.use(authed).handler(({ input, context }) => {
|
||||||
const coreType = watchHistoryTypeMap[input.type];
|
const coreType = watchHistoryTypeMap[input.type];
|
||||||
const count = getWatchCount(context.user.id, coreType, input.period);
|
const count = getWatchCount(context.user.id, coreType, input.period);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const implementedRouter = {
|
|||||||
dashboard: {
|
dashboard: {
|
||||||
stats: dashboard.stats,
|
stats: dashboard.stats,
|
||||||
continueWatching: dashboard.continueWatching,
|
continueWatching: dashboard.continueWatching,
|
||||||
|
upcoming: dashboard.upcoming,
|
||||||
library: dashboard.library,
|
library: dashboard.library,
|
||||||
recommendations: dashboard.recommendations,
|
recommendations: dashboard.recommendations,
|
||||||
watchHistory: dashboard.watchHistory,
|
watchHistory: dashboard.watchHistory,
|
||||||
|
|||||||
@@ -1,19 +1,30 @@
|
|||||||
|
import { Trans } from "@lingui/react/macro";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
export function FeedSection({
|
export function FeedSection({
|
||||||
title,
|
title,
|
||||||
icon,
|
icon,
|
||||||
children,
|
children,
|
||||||
|
seeAllLink,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
seeAllLink?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between">
|
||||||
<span aria-hidden={true}>{icon}</span>
|
<div className="flex items-center gap-2">
|
||||||
<h2 className="font-display text-xl tracking-tight text-balance">{title}</h2>
|
<span aria-hidden={true}>{icon}</span>
|
||||||
|
<h2 className="font-display text-xl tracking-tight text-balance">{title}</h2>
|
||||||
|
</div>
|
||||||
|
{seeAllLink && (
|
||||||
|
<Link to={seeAllLink} className="text-primary text-sm hover:underline">
|
||||||
|
<Trans>See all</Trans>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { plural } from "@lingui/core/macro";
|
||||||
|
import { useLingui } from "@lingui/react/macro";
|
||||||
|
import { IconMovie } from "@tabler/icons-react";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { thumbHashToUrl } from "@/lib/thumbhash";
|
||||||
|
import type { UpcomingItem } from "@sofa/api/schemas";
|
||||||
|
|
||||||
|
const statusColorClass = {
|
||||||
|
in_watchlist: "bg-status-watchlist",
|
||||||
|
watching: "bg-status-watching",
|
||||||
|
caught_up: "bg-status-completed",
|
||||||
|
completed: "bg-status-completed",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const statusHaloClass = statusColorClass;
|
||||||
|
|
||||||
|
function formatShortDate(dateStr: string): string {
|
||||||
|
const d = new Date(`${dateStr}T00:00:00Z`);
|
||||||
|
return new Intl.DateTimeFormat(undefined, {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
timeZone: "UTC",
|
||||||
|
}).format(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UpcomingRow({ item }: { item: UpcomingItem }) {
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
const statusLabels = {
|
||||||
|
in_watchlist: t`On Watchlist`,
|
||||||
|
watching: t`Watching`,
|
||||||
|
caught_up: t`Caught Up`,
|
||||||
|
completed: t`Completed`,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
let subtitle: string;
|
||||||
|
if (item.titleType === "movie") {
|
||||||
|
subtitle = formatShortDate(item.date);
|
||||||
|
} else if (item.episodeCount > 1 && item.seasonNumber != null) {
|
||||||
|
const seasonNum = item.seasonNumber;
|
||||||
|
const epCount = item.episodeCount;
|
||||||
|
subtitle = t`S${seasonNum} \u00b7 ${plural(epCount, { one: "# episode", other: "# episodes" })}`;
|
||||||
|
} else {
|
||||||
|
const episodeLabel =
|
||||||
|
item.seasonNumber != null && item.episodeNumber != null
|
||||||
|
? `S${item.seasonNumber}E${item.episodeNumber}`
|
||||||
|
: null;
|
||||||
|
subtitle = [episodeLabel, item.episodeName].filter(Boolean).join(" \u00b7 ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to="/titles/$id"
|
||||||
|
params={{ id: item.titleId }}
|
||||||
|
className="group bg-card/40 hover:bg-card/60 hover:shadow-primary/5 hover:ring-primary/25 flex items-center gap-4 rounded-xl px-3 py-3 ring-1 ring-white/[0.06] transition-[background,box-shadow,ring-color] duration-200 hover:shadow-lg"
|
||||||
|
>
|
||||||
|
{/* Poster */}
|
||||||
|
<div className="relative h-[66px] w-11 shrink-0 overflow-hidden rounded-lg ring-1 ring-white/[0.06]">
|
||||||
|
{item.posterPath ? (
|
||||||
|
<img
|
||||||
|
src={item.posterPath}
|
||||||
|
alt=""
|
||||||
|
className="size-full object-cover motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-105"
|
||||||
|
loading="lazy"
|
||||||
|
{...(item.posterThumbHash
|
||||||
|
? {
|
||||||
|
style: {
|
||||||
|
background: `url(${thumbHashToUrl(item.posterThumbHash)}) center/cover`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {})}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="bg-muted flex size-full items-center justify-center">
|
||||||
|
<IconMovie className="text-muted-foreground size-5" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger className="relative flex size-2 shrink-0">
|
||||||
|
<span
|
||||||
|
className={`absolute inline-flex size-full rounded-full opacity-40 motion-safe:animate-pulse ${statusHaloClass[item.userStatus]}`}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`relative inline-flex size-2 rounded-full ${statusColorClass[item.userStatus]}`}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">{statusLabels[item.userStatus]}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<span className="truncate text-sm font-medium">{item.titleName}</span>
|
||||||
|
{item.isNewSeason && (
|
||||||
|
<span className="bg-primary/15 text-primary shrink-0 rounded px-1.5 py-0.5 text-[10px] font-semibold tracking-wider uppercase">
|
||||||
|
{t`New Season`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground mt-1 flex items-center gap-1.5 text-xs">
|
||||||
|
{item.titleType === "movie" && <IconMovie className="size-3 shrink-0" />}
|
||||||
|
<span className="truncate">{subtitle}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right column: date + provider logo */}
|
||||||
|
<div className="flex shrink-0 flex-col items-end gap-1">
|
||||||
|
<span className="text-muted-foreground text-xs">{formatShortDate(item.date)}</span>
|
||||||
|
{item.streamingProvider && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger className="shrink-0">
|
||||||
|
{item.streamingProvider.logoPath ? (
|
||||||
|
<img
|
||||||
|
src={item.streamingProvider.logoPath}
|
||||||
|
alt={item.streamingProvider.providerName}
|
||||||
|
className="size-7 rounded-lg ring-1 ring-white/[0.06]"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground/60 text-[10px]">
|
||||||
|
{item.streamingProvider.providerName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">{item.streamingProvider.providerName}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { useLingui } from "@lingui/react/macro";
|
||||||
|
import { IconCalendarEvent } from "@tabler/icons-react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { orpc } from "@/lib/orpc/client";
|
||||||
|
|
||||||
|
import { FeedSection } from "./feed-section";
|
||||||
|
import { UpcomingRow } from "./upcoming-item";
|
||||||
|
|
||||||
|
export function UpcomingSection() {
|
||||||
|
const { data, isPending } = useQuery(
|
||||||
|
orpc.dashboard.upcoming.queryOptions({ input: { days: 7, limit: 5 } }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
if (isPending) return null;
|
||||||
|
|
||||||
|
const items = data?.items ?? [];
|
||||||
|
if (items.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FeedSection
|
||||||
|
title={t`Upcoming`}
|
||||||
|
icon={<IconCalendarEvent className="text-primary size-5" />}
|
||||||
|
seeAllLink="/upcoming"
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<UpcomingRow key={`${item.titleId}-${item.date}-${i}`} item={item} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</FeedSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
import { Trans, useLingui } from "@lingui/react/macro";
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
import { IconCompass, IconHome, IconLogout, IconSearch, IconSettings } from "@tabler/icons-react";
|
import {
|
||||||
|
IconCalendarEvent,
|
||||||
|
IconCompass,
|
||||||
|
IconHome,
|
||||||
|
IconLogout,
|
||||||
|
IconSearch,
|
||||||
|
IconSettings,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
import { Link, useLocation, useNavigate } from "@tanstack/react-router";
|
import { Link, useLocation, useNavigate } from "@tanstack/react-router";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
@@ -67,7 +74,6 @@ function useActiveIndicator<T>(
|
|||||||
const instantRef = useRef(true);
|
const instantRef = useRef(true);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
instantRef.current = false;
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
if (activeIndex === -1) {
|
if (activeIndex === -1) {
|
||||||
setValue(null);
|
setValue(null);
|
||||||
@@ -82,6 +88,8 @@ function useActiveIndicator<T>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
update();
|
update();
|
||||||
|
// After the initial measurement, allow subsequent changes to animate
|
||||||
|
instantRef.current = false;
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
@@ -114,6 +122,7 @@ export function NavBar({
|
|||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ href: "/dashboard", label: t`Home` },
|
{ href: "/dashboard", label: t`Home` },
|
||||||
{ href: "/explore", label: t`Explore` },
|
{ href: "/explore", label: t`Explore` },
|
||||||
|
{ href: "/upcoming", label: t`Upcoming` },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const initial = userName?.charAt(0).toUpperCase() ?? "?";
|
const initial = userName?.charAt(0).toUpperCase() ?? "?";
|
||||||
@@ -275,6 +284,7 @@ export function MobileTabBar() {
|
|||||||
const mobileTabs = [
|
const mobileTabs = [
|
||||||
{ href: "/dashboard", label: t`Home`, icon: IconHome },
|
{ href: "/dashboard", label: t`Home`, icon: IconHome },
|
||||||
{ href: "/explore", label: t`Explore`, icon: IconCompass },
|
{ href: "/explore", label: t`Explore`, icon: IconCompass },
|
||||||
|
{ href: "/upcoming", label: t`Upcoming`, icon: IconCalendarEvent },
|
||||||
{ href: "/settings", label: t`Settings`, icon: IconSettings },
|
{ href: "/settings", label: t`Settings`, icon: IconSettings },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { PersonHero } from "./person-hero";
|
|||||||
|
|
||||||
export function PersonDetailSkeleton() {
|
export function PersonDetailSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col gap-6 sm:flex-row sm:gap-8">
|
<div className="flex flex-col gap-6 sm:flex-row sm:gap-8">
|
||||||
<Skeleton className="size-40 shrink-0 self-center rounded-2xl sm:size-56 sm:self-start" />
|
<Skeleton className="size-40 shrink-0 self-center rounded-2xl sm:size-56 sm:self-start" />
|
||||||
<div className="flex-1 space-y-3">
|
<div className="flex-1 space-y-3">
|
||||||
@@ -62,7 +62,7 @@ export function PersonDetailClient({ id }: { id: string }) {
|
|||||||
if (!person) return null;
|
if (!person) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-6">
|
||||||
<PersonHero person={person} />
|
<PersonHero person={person} />
|
||||||
<FilmographyGrid credits={filmography} userStatuses={userStatuses} />
|
<FilmographyGrid credits={filmography} userStatuses={userStatuses} />
|
||||||
<div ref={sentinelRef} />
|
<div ref={sentinelRef} />
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const sectionVariants = {
|
|||||||
export function SettingsShell({ children, footer }: { children: ReactNode; footer?: ReactNode }) {
|
export function SettingsShell({ children, footer }: { children: ReactNode; footer?: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="mx-auto max-w-2xl space-y-8"
|
className="mx-auto max-w-2xl space-y-6"
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
variants={{
|
variants={{
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Route as AppRouteImport } from './routes/_app'
|
|||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as AuthRegisterRouteImport } from './routes/_auth/register'
|
import { Route as AuthRegisterRouteImport } from './routes/_auth/register'
|
||||||
import { Route as AuthLoginRouteImport } from './routes/_auth/login'
|
import { Route as AuthLoginRouteImport } from './routes/_auth/login'
|
||||||
|
import { Route as AppUpcomingRouteImport } from './routes/_app/upcoming'
|
||||||
import { Route as AppSettingsRouteImport } from './routes/_app/settings'
|
import { Route as AppSettingsRouteImport } from './routes/_app/settings'
|
||||||
import { Route as AppExploreRouteImport } from './routes/_app/explore'
|
import { Route as AppExploreRouteImport } from './routes/_app/explore'
|
||||||
import { Route as AppDashboardRouteImport } from './routes/_app/dashboard'
|
import { Route as AppDashboardRouteImport } from './routes/_app/dashboard'
|
||||||
@@ -49,6 +50,11 @@ const AuthLoginRoute = AuthLoginRouteImport.update({
|
|||||||
path: '/login',
|
path: '/login',
|
||||||
getParentRoute: () => AuthRoute,
|
getParentRoute: () => AuthRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AppUpcomingRoute = AppUpcomingRouteImport.update({
|
||||||
|
id: '/upcoming',
|
||||||
|
path: '/upcoming',
|
||||||
|
getParentRoute: () => AppRoute,
|
||||||
|
} as any)
|
||||||
const AppSettingsRoute = AppSettingsRouteImport.update({
|
const AppSettingsRoute = AppSettingsRouteImport.update({
|
||||||
id: '/settings',
|
id: '/settings',
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
@@ -81,6 +87,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/dashboard': typeof AppDashboardRoute
|
'/dashboard': typeof AppDashboardRoute
|
||||||
'/explore': typeof AppExploreRoute
|
'/explore': typeof AppExploreRoute
|
||||||
'/settings': typeof AppSettingsRoute
|
'/settings': typeof AppSettingsRoute
|
||||||
|
'/upcoming': typeof AppUpcomingRoute
|
||||||
'/login': typeof AuthLoginRoute
|
'/login': typeof AuthLoginRoute
|
||||||
'/register': typeof AuthRegisterRoute
|
'/register': typeof AuthRegisterRoute
|
||||||
'/people/$id': typeof AppPeopleIdRoute
|
'/people/$id': typeof AppPeopleIdRoute
|
||||||
@@ -92,6 +99,7 @@ export interface FileRoutesByTo {
|
|||||||
'/dashboard': typeof AppDashboardRoute
|
'/dashboard': typeof AppDashboardRoute
|
||||||
'/explore': typeof AppExploreRoute
|
'/explore': typeof AppExploreRoute
|
||||||
'/settings': typeof AppSettingsRoute
|
'/settings': typeof AppSettingsRoute
|
||||||
|
'/upcoming': typeof AppUpcomingRoute
|
||||||
'/login': typeof AuthLoginRoute
|
'/login': typeof AuthLoginRoute
|
||||||
'/register': typeof AuthRegisterRoute
|
'/register': typeof AuthRegisterRoute
|
||||||
'/people/$id': typeof AppPeopleIdRoute
|
'/people/$id': typeof AppPeopleIdRoute
|
||||||
@@ -106,6 +114,7 @@ export interface FileRoutesById {
|
|||||||
'/_app/dashboard': typeof AppDashboardRoute
|
'/_app/dashboard': typeof AppDashboardRoute
|
||||||
'/_app/explore': typeof AppExploreRoute
|
'/_app/explore': typeof AppExploreRoute
|
||||||
'/_app/settings': typeof AppSettingsRoute
|
'/_app/settings': typeof AppSettingsRoute
|
||||||
|
'/_app/upcoming': typeof AppUpcomingRoute
|
||||||
'/_auth/login': typeof AuthLoginRoute
|
'/_auth/login': typeof AuthLoginRoute
|
||||||
'/_auth/register': typeof AuthRegisterRoute
|
'/_auth/register': typeof AuthRegisterRoute
|
||||||
'/_app/people/$id': typeof AppPeopleIdRoute
|
'/_app/people/$id': typeof AppPeopleIdRoute
|
||||||
@@ -119,6 +128,7 @@ export interface FileRouteTypes {
|
|||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/explore'
|
| '/explore'
|
||||||
| '/settings'
|
| '/settings'
|
||||||
|
| '/upcoming'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/register'
|
| '/register'
|
||||||
| '/people/$id'
|
| '/people/$id'
|
||||||
@@ -130,6 +140,7 @@ export interface FileRouteTypes {
|
|||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/explore'
|
| '/explore'
|
||||||
| '/settings'
|
| '/settings'
|
||||||
|
| '/upcoming'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/register'
|
| '/register'
|
||||||
| '/people/$id'
|
| '/people/$id'
|
||||||
@@ -143,6 +154,7 @@ export interface FileRouteTypes {
|
|||||||
| '/_app/dashboard'
|
| '/_app/dashboard'
|
||||||
| '/_app/explore'
|
| '/_app/explore'
|
||||||
| '/_app/settings'
|
| '/_app/settings'
|
||||||
|
| '/_app/upcoming'
|
||||||
| '/_auth/login'
|
| '/_auth/login'
|
||||||
| '/_auth/register'
|
| '/_auth/register'
|
||||||
| '/_app/people/$id'
|
| '/_app/people/$id'
|
||||||
@@ -200,6 +212,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthLoginRouteImport
|
preLoaderRoute: typeof AuthLoginRouteImport
|
||||||
parentRoute: typeof AuthRoute
|
parentRoute: typeof AuthRoute
|
||||||
}
|
}
|
||||||
|
'/_app/upcoming': {
|
||||||
|
id: '/_app/upcoming'
|
||||||
|
path: '/upcoming'
|
||||||
|
fullPath: '/upcoming'
|
||||||
|
preLoaderRoute: typeof AppUpcomingRouteImport
|
||||||
|
parentRoute: typeof AppRoute
|
||||||
|
}
|
||||||
'/_app/settings': {
|
'/_app/settings': {
|
||||||
id: '/_app/settings'
|
id: '/_app/settings'
|
||||||
path: '/settings'
|
path: '/settings'
|
||||||
@@ -242,6 +261,7 @@ interface AppRouteChildren {
|
|||||||
AppDashboardRoute: typeof AppDashboardRoute
|
AppDashboardRoute: typeof AppDashboardRoute
|
||||||
AppExploreRoute: typeof AppExploreRoute
|
AppExploreRoute: typeof AppExploreRoute
|
||||||
AppSettingsRoute: typeof AppSettingsRoute
|
AppSettingsRoute: typeof AppSettingsRoute
|
||||||
|
AppUpcomingRoute: typeof AppUpcomingRoute
|
||||||
AppPeopleIdRoute: typeof AppPeopleIdRoute
|
AppPeopleIdRoute: typeof AppPeopleIdRoute
|
||||||
AppTitlesIdRoute: typeof AppTitlesIdRoute
|
AppTitlesIdRoute: typeof AppTitlesIdRoute
|
||||||
}
|
}
|
||||||
@@ -250,6 +270,7 @@ const AppRouteChildren: AppRouteChildren = {
|
|||||||
AppDashboardRoute: AppDashboardRoute,
|
AppDashboardRoute: AppDashboardRoute,
|
||||||
AppExploreRoute: AppExploreRoute,
|
AppExploreRoute: AppExploreRoute,
|
||||||
AppSettingsRoute: AppSettingsRoute,
|
AppSettingsRoute: AppSettingsRoute,
|
||||||
|
AppUpcomingRoute: AppUpcomingRoute,
|
||||||
AppPeopleIdRoute: AppPeopleIdRoute,
|
AppPeopleIdRoute: AppPeopleIdRoute,
|
||||||
AppTitlesIdRoute: AppTitlesIdRoute,
|
AppTitlesIdRoute: AppTitlesIdRoute,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { RecommendationsSection } from "@/components/dashboard/recommendations-s
|
|||||||
import { StatsSectionSkeleton } from "@/components/dashboard/stats-display";
|
import { StatsSectionSkeleton } from "@/components/dashboard/stats-display";
|
||||||
import { StatsSection } from "@/components/dashboard/stats-section";
|
import { StatsSection } from "@/components/dashboard/stats-section";
|
||||||
import { TitleGridSectionSkeleton } from "@/components/dashboard/title-grid";
|
import { TitleGridSectionSkeleton } from "@/components/dashboard/title-grid";
|
||||||
|
import { UpcomingSection } from "@/components/dashboard/upcoming-section";
|
||||||
import { WelcomeHeader } from "@/components/dashboard/welcome-header";
|
import { WelcomeHeader } from "@/components/dashboard/welcome-header";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { orpc } from "@/lib/orpc/client";
|
import { orpc } from "@/lib/orpc/client";
|
||||||
@@ -19,6 +20,17 @@ export const Route = createFileRoute("/_app/dashboard")({
|
|||||||
context.queryClient.ensureQueryData(orpc.dashboard.stats.queryOptions()),
|
context.queryClient.ensureQueryData(orpc.dashboard.stats.queryOptions()),
|
||||||
context.queryClient.ensureQueryData(orpc.dashboard.continueWatching.queryOptions()),
|
context.queryClient.ensureQueryData(orpc.dashboard.continueWatching.queryOptions()),
|
||||||
context.queryClient.ensureQueryData(orpc.dashboard.recommendations.queryOptions()),
|
context.queryClient.ensureQueryData(orpc.dashboard.recommendations.queryOptions()),
|
||||||
|
context.queryClient.ensureQueryData(
|
||||||
|
orpc.dashboard.upcoming.queryOptions({ input: { days: 7, limit: 5 } }),
|
||||||
|
),
|
||||||
|
context.queryClient.ensureInfiniteQueryData(
|
||||||
|
orpc.dashboard.library.infiniteOptions({
|
||||||
|
input: (pageParam: number) => ({ page: pageParam }),
|
||||||
|
initialPageParam: 1,
|
||||||
|
getNextPageParam: (lastPage) =>
|
||||||
|
lastPage.page < lastPage.totalPages ? lastPage.page + 1 : undefined,
|
||||||
|
}),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
pendingComponent: DashboardSkeleton,
|
pendingComponent: DashboardSkeleton,
|
||||||
@@ -27,7 +39,7 @@ export const Route = createFileRoute("/_app/dashboard")({
|
|||||||
|
|
||||||
function DashboardSkeleton() {
|
function DashboardSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<Skeleton className="h-8 w-64" />
|
<Skeleton className="h-8 w-64" />
|
||||||
<Skeleton className="mt-2 h-4 w-48" />
|
<Skeleton className="mt-2 h-4 w-48" />
|
||||||
@@ -43,10 +55,11 @@ function DashboardSkeleton() {
|
|||||||
function DashboardPage() {
|
function DashboardPage() {
|
||||||
const { session } = Route.useRouteContext();
|
const { session } = Route.useRouteContext();
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-6">
|
||||||
<WelcomeHeader name={session.user.name} />
|
<WelcomeHeader name={session.user.name} />
|
||||||
<StatsSection />
|
<StatsSection />
|
||||||
<ContinueWatchingSection />
|
<ContinueWatchingSection />
|
||||||
|
<UpcomingSection />
|
||||||
<LibrarySection />
|
<LibrarySection />
|
||||||
<RecommendationsSection />
|
<RecommendationsSection />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ export const Route = createFileRoute("/_app/explore")({
|
|||||||
head: () => ({ meta: [{ title: "Explore — Sofa" }] }),
|
head: () => ({ meta: [{ title: "Explore — Sofa" }] }),
|
||||||
loader: async ({ context }) => {
|
loader: async ({ context }) => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
context.queryClient.ensureInfiniteQueryData(
|
||||||
|
orpc.explore.trending.infiniteOptions({
|
||||||
|
input: (pageParam: number) => ({ type: "all" as const, page: pageParam }),
|
||||||
|
initialPageParam: 1,
|
||||||
|
getNextPageParam: (lastPage) =>
|
||||||
|
lastPage.page < lastPage.totalPages ? lastPage.page + 1 : undefined,
|
||||||
|
}),
|
||||||
|
),
|
||||||
context.queryClient.ensureQueryData(
|
context.queryClient.ensureQueryData(
|
||||||
orpc.explore.popular.queryOptions({ input: { type: "movie" } }),
|
orpc.explore.popular.queryOptions({ input: { type: "movie" } }),
|
||||||
),
|
),
|
||||||
@@ -35,7 +43,7 @@ export const Route = createFileRoute("/_app/explore")({
|
|||||||
|
|
||||||
function ExploreSkeletons() {
|
function ExploreSkeletons() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-6">
|
||||||
<Skeleton className="-mt-6 mr-[calc(-50vw+50%)] ml-[calc(-50vw+50%)] h-[320px] rounded-none" />
|
<Skeleton className="-mt-6 mr-[calc(-50vw+50%)] ml-[calc(-50vw+50%)] h-[320px] rounded-none" />
|
||||||
{[1, 2, 3].map((i) => (
|
{[1, 2, 3].map((i) => (
|
||||||
<div key={i} className="space-y-4">
|
<div key={i} className="space-y-4">
|
||||||
@@ -114,7 +122,7 @@ function ExplorePage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-6">
|
||||||
{hero && (
|
{hero && (
|
||||||
<HeroBanner
|
<HeroBanner
|
||||||
id={hero.id}
|
id={hero.id}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const Route = createFileRoute("/_app/settings")({
|
|||||||
|
|
||||||
function SettingsSkeleton() {
|
function SettingsSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-2xl space-y-8">
|
<div className="mx-auto max-w-2xl space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Skeleton className="size-5 rounded" />
|
<Skeleton className="size-5 rounded" />
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function TitleDetailPage() {
|
|||||||
const themeStyle = getThemeCssProperties(title.colorPalette);
|
const themeStyle = getThemeCssProperties(title.colorPalette);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative space-y-10" style={themeStyle}>
|
<div className="relative space-y-6" style={themeStyle}>
|
||||||
<TitleTheme style={themeStyle as Record<string, string>} />
|
<TitleTheme style={themeStyle as Record<string, string>} />
|
||||||
<TitleProvider
|
<TitleProvider
|
||||||
key={title.id}
|
key={title.id}
|
||||||
@@ -74,7 +74,7 @@ function TitleDetailPage() {
|
|||||||
|
|
||||||
function TitleDetailLoading() {
|
function TitleDetailLoading() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-6">
|
||||||
<Skeleton className="-mt-6 mr-[calc(-50vw+50%)] ml-[calc(-50vw+50%)] h-80 rounded-none md:h-[28rem]" />
|
<Skeleton className="-mt-6 mr-[calc(-50vw+50%)] ml-[calc(-50vw+50%)] h-80 rounded-none md:h-[28rem]" />
|
||||||
<div className="flex flex-col gap-4 md:flex-row md:gap-8">
|
<div className="flex flex-col gap-4 md:flex-row md:gap-8">
|
||||||
<Skeleton className="aspect-[2/3] w-[140px] shrink-0 self-center rounded-xl md:w-[220px] md:self-start" />
|
<Skeleton className="aspect-[2/3] w-[140px] shrink-0 self-center rounded-xl md:w-[220px] md:self-start" />
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { Trans, useLingui } from "@lingui/react/macro";
|
||||||
|
import { IconCalendarEvent } from "@tabler/icons-react";
|
||||||
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { UpcomingRow } from "@/components/dashboard/upcoming-item";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { useInfiniteScroll } from "@/hooks/use-infinite-scroll";
|
||||||
|
import { orpc } from "@/lib/orpc/client";
|
||||||
|
import { groupByDateBucket } from "@sofa/i18n/date-buckets";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_app/upcoming")({
|
||||||
|
staleTime: 30_000,
|
||||||
|
head: () => ({ meta: [{ title: "Upcoming — Sofa" }] }),
|
||||||
|
loader: async ({ context }) => {
|
||||||
|
await context.queryClient.ensureInfiniteQueryData(
|
||||||
|
orpc.dashboard.upcoming.infiniteOptions({
|
||||||
|
input: (pageParam: string | undefined) => ({
|
||||||
|
days: 90,
|
||||||
|
limit: 20,
|
||||||
|
cursor: pageParam,
|
||||||
|
}),
|
||||||
|
initialPageParam: undefined as string | undefined,
|
||||||
|
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
pendingComponent: UpcomingSkeleton,
|
||||||
|
component: UpcomingPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function UpcomingSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<Skeleton className="h-8 w-48" />
|
||||||
|
<Skeleton className="mt-2 h-4 w-64" />
|
||||||
|
</div>
|
||||||
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-3.5 py-2">
|
||||||
|
<Skeleton className="size-[52px] rounded-md" />
|
||||||
|
<div className="flex-1 space-y-1.5">
|
||||||
|
<Skeleton className="h-4 w-40" />
|
||||||
|
<Skeleton className="h-3 w-28" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpcomingPage() {
|
||||||
|
const { data, isPending, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery(
|
||||||
|
orpc.dashboard.upcoming.infiniteOptions({
|
||||||
|
input: (pageParam: string | undefined) => ({
|
||||||
|
days: 90,
|
||||||
|
limit: 20,
|
||||||
|
cursor: pageParam,
|
||||||
|
}),
|
||||||
|
initialPageParam: undefined as string | undefined,
|
||||||
|
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sentinelRef = useInfiniteScroll({
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isPending) return <UpcomingSkeleton />;
|
||||||
|
|
||||||
|
const allItems = data?.pages.flatMap((p) => p.items) ?? [];
|
||||||
|
|
||||||
|
if (allItems.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<UpcomingHeader />
|
||||||
|
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||||
|
<IconCalendarEvent className="text-muted-foreground/40 size-12" />
|
||||||
|
<p className="text-muted-foreground mt-4 text-sm">
|
||||||
|
<Trans>No upcoming episodes or releases in the next 90 days.</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buckets = groupByDateBucket(allItems);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-2xl space-y-6">
|
||||||
|
<UpcomingHeader />
|
||||||
|
{buckets.map((bucket) => (
|
||||||
|
<section key={bucket.key}>
|
||||||
|
<h2 className="font-display text-muted-foreground mb-2 text-sm font-medium tracking-wider uppercase">
|
||||||
|
{bucket.label}
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{bucket.items.map((item, i) => (
|
||||||
|
<UpcomingRow key={`${item.titleId}-${item.date}-${i}`} item={item} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
<div ref={sentinelRef} />
|
||||||
|
{isFetchingNextPage && <UpcomingSkeleton />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpcomingHeader() {
|
||||||
|
const { t } = useLingui();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="font-display text-2xl tracking-tight">{t`Upcoming`}</h1>
|
||||||
|
<p className="text-muted-foreground mt-1 text-sm">
|
||||||
|
<Trans>Episodes and movies coming up in the next 90 days.</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
title: Get upcoming episodes and movies
|
||||||
|
full: true
|
||||||
|
_openapi:
|
||||||
|
method: GET
|
||||||
|
toc: []
|
||||||
|
structuredData:
|
||||||
|
headings: []
|
||||||
|
contents:
|
||||||
|
- content: >-
|
||||||
|
Fetch upcoming episodes and movie releases for titles in the user's
|
||||||
|
library, sorted by date. Supports cursor-based pagination.
|
||||||
|
---
|
||||||
|
|
||||||
|
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||||
|
|
||||||
|
Fetch upcoming episodes and movie releases for titles in the user's library, sorted by date. Supports cursor-based pagination.
|
||||||
|
|
||||||
|
<APIPage document={"./public/openapi.json"} operations={[{"path":"/dashboard/upcoming","method":"get"}]} />
|
||||||
+277
-25
@@ -1787,8 +1787,7 @@
|
|||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"in_progress",
|
"watchlist"
|
||||||
"completed"
|
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -1796,13 +1795,13 @@
|
|||||||
"type": "null"
|
"type": "null"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Tracking status: in_progress (currently watching), completed (finished), or null to remove from library"
|
"description": "Set to watchlist to add to library, or null to remove. Other transitions happen via watch endpoints."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"status"
|
"status"
|
||||||
],
|
],
|
||||||
"description": "Update the user's tracking status for a title"
|
"description": "Add a title to the user's watchlist or remove from library"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1989,8 +1988,9 @@
|
|||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"watchlist",
|
"in_watchlist",
|
||||||
"in_progress",
|
"watching",
|
||||||
|
"caught_up",
|
||||||
"completed"
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1999,7 +1999,7 @@
|
|||||||
"type": "null"
|
"type": "null"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "User's tracking status, or null if not in library"
|
"description": "User's display status, or null if not in library"
|
||||||
},
|
},
|
||||||
"rating": {
|
"rating": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
@@ -2079,13 +2079,14 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"watchlist",
|
"in_watchlist",
|
||||||
"in_progress",
|
"watching",
|
||||||
|
"caught_up",
|
||||||
"completed"
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"description": "Map of title ID to the user's tracking status"
|
"description": "Map of title ID to the user's display status"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -2525,13 +2526,14 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"watchlist",
|
"in_watchlist",
|
||||||
"in_progress",
|
"watching",
|
||||||
|
"caught_up",
|
||||||
"completed"
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"description": "Map of title ID to the user's tracking status"
|
"description": "Map of title ID to the user's display status"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
@@ -2848,6 +2850,252 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/dashboard/upcoming": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "dashboard.upcoming",
|
||||||
|
"summary": "Get upcoming episodes and movies",
|
||||||
|
"description": "Fetch upcoming episodes and movie releases for titles in the user's library, sorted by date. Supports cursor-based pagination.",
|
||||||
|
"tags": [
|
||||||
|
"Dashboard"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "days",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 90,
|
||||||
|
"default": 90,
|
||||||
|
"description": "How many days into the future to look"
|
||||||
|
},
|
||||||
|
"allowEmptyValue": true,
|
||||||
|
"allowReserved": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 50,
|
||||||
|
"default": 20,
|
||||||
|
"description": "Maximum items per page"
|
||||||
|
},
|
||||||
|
"allowEmptyValue": true,
|
||||||
|
"allowReserved": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cursor",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Pagination cursor"
|
||||||
|
},
|
||||||
|
"allowEmptyValue": true,
|
||||||
|
"allowReserved": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Upcoming items sorted by date with streaming info",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"episodeId": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Episode ID (TV only, null for movies and collapsed batches)"
|
||||||
|
},
|
||||||
|
"titleId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Internal title ID"
|
||||||
|
},
|
||||||
|
"titleName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Display title"
|
||||||
|
},
|
||||||
|
"titleType": {
|
||||||
|
"enum": [
|
||||||
|
"movie",
|
||||||
|
"tv"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"description": "Media type"
|
||||||
|
},
|
||||||
|
"posterPath": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Poster image path"
|
||||||
|
},
|
||||||
|
"posterThumbHash": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "ThumbHash blur placeholder"
|
||||||
|
},
|
||||||
|
"seasonNumber": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Season number (TV only)"
|
||||||
|
},
|
||||||
|
"episodeNumber": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Episode number (TV only)"
|
||||||
|
},
|
||||||
|
"episodeName": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Episode title (TV only)"
|
||||||
|
},
|
||||||
|
"episodeCount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Number of episodes (1 for single, >1 for collapsed batch drops)"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Air date or release date (YYYY-MM-DD)"
|
||||||
|
},
|
||||||
|
"userStatus": {
|
||||||
|
"enum": [
|
||||||
|
"in_watchlist",
|
||||||
|
"watching",
|
||||||
|
"caught_up",
|
||||||
|
"completed"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"description": "User's display status"
|
||||||
|
},
|
||||||
|
"isNewSeason": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether this is a new season for a completed show"
|
||||||
|
},
|
||||||
|
"streamingProvider": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"providerId": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"providerName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"logoPath": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"providerId",
|
||||||
|
"providerName",
|
||||||
|
"logoPath"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Primary streaming provider, or null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"episodeId",
|
||||||
|
"titleId",
|
||||||
|
"titleName",
|
||||||
|
"titleType",
|
||||||
|
"posterPath",
|
||||||
|
"posterThumbHash",
|
||||||
|
"seasonNumber",
|
||||||
|
"episodeNumber",
|
||||||
|
"episodeName",
|
||||||
|
"episodeCount",
|
||||||
|
"date",
|
||||||
|
"userStatus",
|
||||||
|
"isNewSeason",
|
||||||
|
"streamingProvider"
|
||||||
|
],
|
||||||
|
"description": "An upcoming episode or movie release"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nextCursor": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Cursor for the next page, or null if no more items"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"items",
|
||||||
|
"nextCursor"
|
||||||
|
],
|
||||||
|
"description": "Upcoming episodes and movie releases for tracked titles"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"session": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/dashboard/library": {
|
"/dashboard/library": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "dashboard.library",
|
"operationId": "dashboard.library",
|
||||||
@@ -2976,8 +3224,9 @@
|
|||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"watchlist",
|
"in_watchlist",
|
||||||
"in_progress",
|
"watching",
|
||||||
|
"caught_up",
|
||||||
"completed"
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -2986,7 +3235,7 @@
|
|||||||
"type": "null"
|
"type": "null"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "User's tracking status"
|
"description": "User's display status"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -3292,13 +3541,14 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"watchlist",
|
"in_watchlist",
|
||||||
"in_progress",
|
"watching",
|
||||||
|
"caught_up",
|
||||||
"completed"
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"description": "Map of title ID to the user's tracking status"
|
"description": "Map of title ID to the user's display status"
|
||||||
},
|
},
|
||||||
"episodeProgress": {
|
"episodeProgress": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -3488,13 +3738,14 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"watchlist",
|
"in_watchlist",
|
||||||
"in_progress",
|
"watching",
|
||||||
|
"caught_up",
|
||||||
"completed"
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"description": "Map of title ID to the user's tracking status"
|
"description": "Map of title ID to the user's display status"
|
||||||
},
|
},
|
||||||
"episodeProgress": {
|
"episodeProgress": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -4124,13 +4375,14 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"watchlist",
|
"in_watchlist",
|
||||||
"in_progress",
|
"watching",
|
||||||
|
"caught_up",
|
||||||
"completed"
|
"completed"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"description": "Map of title ID to the user's tracking status"
|
"description": "Map of title ID to the user's display status"
|
||||||
},
|
},
|
||||||
"episodeProgress": {
|
"episodeProgress": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ import {
|
|||||||
UpdateRatingInput,
|
UpdateRatingInput,
|
||||||
UpdateScheduleInput,
|
UpdateScheduleInput,
|
||||||
UpdateStatusInput,
|
UpdateStatusInput,
|
||||||
|
UpcomingInput,
|
||||||
|
UpcomingOutput,
|
||||||
UploadAvatarInput,
|
UploadAvatarInput,
|
||||||
UploadAvatarOutput,
|
UploadAvatarOutput,
|
||||||
UserInfoOutput,
|
UserInfoOutput,
|
||||||
@@ -290,6 +292,18 @@ export const contract = {
|
|||||||
successDescription: "Recommended titles",
|
successDescription: "Recommended titles",
|
||||||
})
|
})
|
||||||
.output(DashboardRecommendationsOutput),
|
.output(DashboardRecommendationsOutput),
|
||||||
|
upcoming: oc
|
||||||
|
.route({
|
||||||
|
method: "GET",
|
||||||
|
path: "/dashboard/upcoming",
|
||||||
|
tags: ["Dashboard"],
|
||||||
|
summary: "Get upcoming episodes and movies",
|
||||||
|
description:
|
||||||
|
"Fetch upcoming episodes and movie releases for titles in the user's library, sorted by date. Supports cursor-based pagination.",
|
||||||
|
successDescription: "Upcoming items sorted by date with streaming info",
|
||||||
|
})
|
||||||
|
.input(UpcomingInput)
|
||||||
|
.output(UpcomingOutput),
|
||||||
watchHistory: oc
|
watchHistory: oc
|
||||||
.route({
|
.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|||||||
@@ -540,6 +540,63 @@ export const DashboardRecommendationsOutput = z
|
|||||||
description: "Personalized title recommendations for the dashboard",
|
description: "Personalized title recommendations for the dashboard",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Upcoming outputs ─────────────────────────────────────────
|
||||||
|
|
||||||
|
export const UpcomingInput = z
|
||||||
|
.object({
|
||||||
|
days: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(1)
|
||||||
|
.max(90)
|
||||||
|
.default(90)
|
||||||
|
.describe("How many days into the future to look"),
|
||||||
|
limit: z.number().int().min(1).max(50).default(20).describe("Maximum items per page"),
|
||||||
|
cursor: z.string().optional().describe("Pagination cursor"),
|
||||||
|
})
|
||||||
|
.meta({ description: "Filters for the upcoming feed" });
|
||||||
|
|
||||||
|
export const UpcomingItemSchema = z
|
||||||
|
.object({
|
||||||
|
episodeId: z
|
||||||
|
.string()
|
||||||
|
.nullable()
|
||||||
|
.describe("Episode ID (TV only, null for movies and collapsed batches)"),
|
||||||
|
titleId: z.string().describe("Internal title ID"),
|
||||||
|
titleName: z.string().describe("Display title"),
|
||||||
|
titleType: z.enum(["movie", "tv"]).describe("Media type"),
|
||||||
|
posterPath: z.string().nullable().describe("Poster image path"),
|
||||||
|
posterThumbHash: z.string().nullable().describe("ThumbHash blur placeholder"),
|
||||||
|
seasonNumber: z.number().nullable().describe("Season number (TV only)"),
|
||||||
|
episodeNumber: z.number().nullable().describe("Episode number (TV only)"),
|
||||||
|
episodeName: z.string().nullable().describe("Episode title (TV only)"),
|
||||||
|
episodeCount: z
|
||||||
|
.number()
|
||||||
|
.describe("Number of episodes (1 for single, >1 for collapsed batch drops)"),
|
||||||
|
date: z.string().describe("Air date or release date (YYYY-MM-DD)"),
|
||||||
|
userStatus: displayStatusEnum.describe("User's display status"),
|
||||||
|
isNewSeason: z.boolean().describe("Whether this is a new season for a completed show"),
|
||||||
|
streamingProvider: z
|
||||||
|
.object({
|
||||||
|
providerId: z.number(),
|
||||||
|
providerName: z.string(),
|
||||||
|
logoPath: z.string().nullable(),
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
.describe("Primary streaming provider, or null"),
|
||||||
|
})
|
||||||
|
.meta({ description: "An upcoming episode or movie release" });
|
||||||
|
|
||||||
|
export const UpcomingOutput = z
|
||||||
|
.object({
|
||||||
|
items: z.array(UpcomingItemSchema),
|
||||||
|
nextCursor: z
|
||||||
|
.string()
|
||||||
|
.nullable()
|
||||||
|
.describe("Cursor for the next page, or null if no more items"),
|
||||||
|
})
|
||||||
|
.meta({ description: "Upcoming episodes and movie releases for tracked titles" });
|
||||||
|
|
||||||
// ─── Explore outputs ───────────────────────────────────────────
|
// ─── Explore outputs ───────────────────────────────────────────
|
||||||
|
|
||||||
export const TrendingOutput = z
|
export const TrendingOutput = z
|
||||||
@@ -1036,4 +1093,5 @@ export type PaginationInfo = z.infer<typeof PaginationMeta>;
|
|||||||
export type TimePeriod = z.infer<typeof WatchHistoryInput>["period"];
|
export type TimePeriod = z.infer<typeof WatchHistoryInput>["period"];
|
||||||
export type ImportJob = z.infer<typeof ImportJobSchema>;
|
export type ImportJob = z.infer<typeof ImportJobSchema>;
|
||||||
export type NormalizedImport = z.infer<typeof NormalizedImportSchema>;
|
export type NormalizedImport = z.infer<typeof NormalizedImportSchema>;
|
||||||
|
export type UpcomingItem = z.infer<typeof UpcomingItemSchema>;
|
||||||
export type UpdateCheckResult = z.infer<typeof UpdateCheckResultSchema>;
|
export type UpdateCheckResult = z.infer<typeof UpdateCheckResultSchema>;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import type { DisplayStatus } from "@sofa/api/display-status";
|
||||||
import {
|
import {
|
||||||
getAllTrackedTitleIds,
|
getAllTrackedTitleIds,
|
||||||
|
getAvailabilityByTitleIds,
|
||||||
getEngagedTitleIds,
|
getEngagedTitleIds,
|
||||||
getEpisodesBySeasonIds,
|
getEpisodesBySeasonIds,
|
||||||
getEpisodeWatchCountSince,
|
getEpisodeWatchCountSince,
|
||||||
@@ -17,10 +19,18 @@ import {
|
|||||||
getTitleByIdOrNull,
|
getTitleByIdOrNull,
|
||||||
getTitlesByIds,
|
getTitlesByIds,
|
||||||
getTvTitlesByIds,
|
getTvTitlesByIds,
|
||||||
|
getUpcomingEpisodes,
|
||||||
|
getUpcomingMovies,
|
||||||
getUserStatusCounts,
|
getUserStatusCounts,
|
||||||
} from "@sofa/db/queries/discovery";
|
} from "@sofa/db/queries/discovery";
|
||||||
import { tmdbImageUrl } from "@sofa/tmdb/image";
|
import { tmdbImageUrl } from "@sofa/tmdb/image";
|
||||||
|
|
||||||
|
import { getDisplayStatusesByTitleIds } from "./tracking";
|
||||||
|
|
||||||
|
function formatLocalDate(d: Date): string {
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
export type TimePeriod = "today" | "this_week" | "this_month" | "this_year";
|
export type TimePeriod = "today" | "this_week" | "this_month" | "this_year";
|
||||||
|
|
||||||
export function periodStartTimestamp(period: TimePeriod): number {
|
export function periodStartTimestamp(period: TimePeriod): number {
|
||||||
@@ -340,6 +350,217 @@ export function getRecommendationsFeed(userId: string) {
|
|||||||
return sorted.map((r) => recTitleMap.get(r.titleId)).filter(Boolean);
|
return sorted.map((r) => recTitleMap.get(r.titleId)).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Upcoming feed ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface UpcomingItem {
|
||||||
|
episodeId: string | null;
|
||||||
|
titleId: string;
|
||||||
|
titleName: string;
|
||||||
|
titleType: "movie" | "tv";
|
||||||
|
posterPath: string | null;
|
||||||
|
posterThumbHash: string | null;
|
||||||
|
seasonNumber: number | null;
|
||||||
|
episodeNumber: number | null;
|
||||||
|
episodeName: string | null;
|
||||||
|
episodeCount: number;
|
||||||
|
date: string;
|
||||||
|
userStatus: DisplayStatus;
|
||||||
|
isNewSeason: boolean;
|
||||||
|
streamingProvider: {
|
||||||
|
providerId: number;
|
||||||
|
providerName: string;
|
||||||
|
logoPath: string | null;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpcomingFeedResult {
|
||||||
|
items: UpcomingItem[];
|
||||||
|
nextCursor: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUpcomingFeed(
|
||||||
|
userId: string,
|
||||||
|
options: { days?: number; limit?: number; cursor?: string } = {},
|
||||||
|
): UpcomingFeedResult {
|
||||||
|
const { days = 90, limit = 20, cursor } = options;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const today = formatLocalDate(now);
|
||||||
|
const horizon = new Date();
|
||||||
|
horizon.setDate(horizon.getDate() + days);
|
||||||
|
const toDate = formatLocalDate(horizon);
|
||||||
|
|
||||||
|
// Use the cursor date as the lower bound so later pages skip already-seen dates,
|
||||||
|
// but don't apply a DB-level LIMIT so same-day items aren't truncated.
|
||||||
|
let cursorDate: string | undefined;
|
||||||
|
let cursorName: string | undefined;
|
||||||
|
let cursorId: string | undefined;
|
||||||
|
if (cursor) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(atob(cursor));
|
||||||
|
cursorDate = parsed.d;
|
||||||
|
cursorName = parsed.n;
|
||||||
|
cursorId = parsed.i;
|
||||||
|
} catch {
|
||||||
|
// Invalid cursor — ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const fromDate = cursorDate ?? today;
|
||||||
|
const episodeRows = getUpcomingEpisodes(userId, fromDate, toDate);
|
||||||
|
const movieRows = getUpcomingMovies(userId, fromDate, toDate);
|
||||||
|
|
||||||
|
// Merge into unified items
|
||||||
|
type RawItem = { date: string; titleId: string; titleName: string } & (
|
||||||
|
| { type: "tv"; row: (typeof episodeRows)[number] }
|
||||||
|
| { type: "movie"; row: (typeof movieRows)[number] }
|
||||||
|
);
|
||||||
|
|
||||||
|
const merged: RawItem[] = [
|
||||||
|
...episodeRows.map((r) => ({
|
||||||
|
date: r.airDate!,
|
||||||
|
titleId: r.titleId,
|
||||||
|
titleName: r.titleName,
|
||||||
|
type: "tv" as const,
|
||||||
|
row: r,
|
||||||
|
})),
|
||||||
|
...movieRows.map((r) => ({
|
||||||
|
date: r.releaseDate!,
|
||||||
|
titleId: r.titleId,
|
||||||
|
titleName: r.titleName,
|
||||||
|
type: "movie" as const,
|
||||||
|
row: r,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sort by date ASC, then title ASC, then titleId for deterministic tiebreak
|
||||||
|
merged.sort(
|
||||||
|
(a, b) =>
|
||||||
|
a.date.localeCompare(b.date) ||
|
||||||
|
a.titleName.localeCompare(b.titleName) ||
|
||||||
|
a.titleId.localeCompare(b.titleId),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Collapse batch drops: group 3+ episodes from the same title on the same date into one item
|
||||||
|
const collapsed: (RawItem & { episodeCount: number })[] = [];
|
||||||
|
let i = 0;
|
||||||
|
while (i < merged.length) {
|
||||||
|
const current = merged[i]!;
|
||||||
|
if (current.type === "tv") {
|
||||||
|
// Count consecutive episodes with the same (titleId, date)
|
||||||
|
let groupEnd = i + 1;
|
||||||
|
while (
|
||||||
|
groupEnd < merged.length &&
|
||||||
|
merged[groupEnd]!.type === "tv" &&
|
||||||
|
merged[groupEnd]!.titleId === current.titleId &&
|
||||||
|
merged[groupEnd]!.date === current.date
|
||||||
|
) {
|
||||||
|
groupEnd++;
|
||||||
|
}
|
||||||
|
const groupSize = groupEnd - i;
|
||||||
|
if (groupSize >= 3) {
|
||||||
|
// Collapse: keep first item with episodeCount and clear episode name
|
||||||
|
collapsed.push({ ...current, episodeCount: groupSize });
|
||||||
|
i = groupEnd;
|
||||||
|
} else {
|
||||||
|
// Keep individual items
|
||||||
|
for (let j = i; j < groupEnd; j++) {
|
||||||
|
collapsed.push({ ...merged[j]!, episodeCount: 1 });
|
||||||
|
}
|
||||||
|
i = groupEnd;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
collapsed.push({ ...current, episodeCount: 1 });
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply cursor: skip items at or before the cursor position.
|
||||||
|
// Cursor is base64-encoded JSON {d, n, i} matching the sort key (date, titleName, titleId).
|
||||||
|
let startIdx = 0;
|
||||||
|
if (cursorDate && cursorName && cursorId) {
|
||||||
|
startIdx = collapsed.findIndex(
|
||||||
|
(item) =>
|
||||||
|
item.date > cursorDate ||
|
||||||
|
(item.date === cursorDate && item.titleName > cursorName) ||
|
||||||
|
(item.date === cursorDate && item.titleName === cursorName && item.titleId > cursorId),
|
||||||
|
);
|
||||||
|
if (startIdx === -1) startIdx = collapsed.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageItems = collapsed.slice(startIdx, startIdx + limit);
|
||||||
|
const hasMore = startIdx + limit < collapsed.length;
|
||||||
|
|
||||||
|
// Compute cursor from the last item on this page
|
||||||
|
let nextCursor: string | null = null;
|
||||||
|
if (hasMore && pageItems.length > 0) {
|
||||||
|
const last = pageItems[pageItems.length - 1]!;
|
||||||
|
nextCursor = btoa(JSON.stringify({ d: last.date, n: last.titleName, i: last.titleId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch-fetch display statuses and streaming providers
|
||||||
|
const titleIds = [...new Set(pageItems.map((item) => item.titleId))];
|
||||||
|
const displayStatuses = getDisplayStatusesByTitleIds(userId, titleIds);
|
||||||
|
const providerRows = getAvailabilityByTitleIds(titleIds);
|
||||||
|
const providerMap = new Map<
|
||||||
|
string,
|
||||||
|
{ providerId: number; providerName: string; logoPath: string | null }
|
||||||
|
>();
|
||||||
|
for (const p of providerRows) {
|
||||||
|
if (!providerMap.has(p.titleId)) {
|
||||||
|
providerMap.set(p.titleId, {
|
||||||
|
providerId: p.providerId,
|
||||||
|
providerName: p.providerName,
|
||||||
|
logoPath: p.logoPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const items: UpcomingItem[] = pageItems.map((item) => {
|
||||||
|
if (item.type === "tv") {
|
||||||
|
const r = item.row;
|
||||||
|
const isCollapsed = item.episodeCount > 1;
|
||||||
|
return {
|
||||||
|
episodeId: isCollapsed ? null : r.episodeId,
|
||||||
|
titleId: r.titleId,
|
||||||
|
titleName: r.titleName,
|
||||||
|
titleType: "tv",
|
||||||
|
posterPath: r.posterPath,
|
||||||
|
posterThumbHash: r.posterThumbHash,
|
||||||
|
seasonNumber: r.seasonNumber,
|
||||||
|
episodeNumber: r.episodeNumber,
|
||||||
|
episodeName: isCollapsed ? null : r.episodeName,
|
||||||
|
episodeCount: item.episodeCount,
|
||||||
|
date: r.airDate!,
|
||||||
|
userStatus: displayStatuses[r.titleId] ?? "watching",
|
||||||
|
isNewSeason:
|
||||||
|
(displayStatuses[r.titleId] === "caught_up" ||
|
||||||
|
displayStatuses[r.titleId] === "completed") &&
|
||||||
|
r.episodeNumber === 1,
|
||||||
|
streamingProvider: providerMap.get(r.titleId) ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const r = item.row;
|
||||||
|
return {
|
||||||
|
episodeId: null,
|
||||||
|
titleId: r.titleId,
|
||||||
|
titleName: r.titleName,
|
||||||
|
titleType: "movie",
|
||||||
|
posterPath: r.posterPath,
|
||||||
|
posterThumbHash: r.posterThumbHash,
|
||||||
|
seasonNumber: null,
|
||||||
|
episodeNumber: null,
|
||||||
|
episodeName: null,
|
||||||
|
episodeCount: 1,
|
||||||
|
date: r.releaseDate!,
|
||||||
|
userStatus: displayStatuses[r.titleId] ?? "in_watchlist",
|
||||||
|
isNewSeason: false,
|
||||||
|
streamingProvider: providerMap.get(r.titleId) ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { items, nextCursor };
|
||||||
|
}
|
||||||
|
|
||||||
export function getRecommendationsForTitle(titleId: string) {
|
export function getRecommendationsForTitle(titleId: string) {
|
||||||
const title = getTitleByIdOrNull(titleId);
|
const title = getTitleByIdOrNull(titleId);
|
||||||
if (!title) return [];
|
if (!title) return [];
|
||||||
|
|||||||
@@ -0,0 +1,293 @@
|
|||||||
|
import { beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
clearAllTables,
|
||||||
|
insertAvailabilityOffer,
|
||||||
|
insertEpisodeWatch,
|
||||||
|
insertStatus,
|
||||||
|
insertTitle,
|
||||||
|
insertTvShow,
|
||||||
|
insertUser,
|
||||||
|
} from "@sofa/db/test-utils";
|
||||||
|
|
||||||
|
import { getUpcomingFeed } from "../src/discovery";
|
||||||
|
|
||||||
|
function daysFromNow(offset: number): string {
|
||||||
|
const d = new Date();
|
||||||
|
d.setDate(d.getDate() + offset);
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
clearAllTables();
|
||||||
|
insertUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Basic feed ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("getUpcomingFeed", () => {
|
||||||
|
test("returns upcoming episodes for tracked shows", () => {
|
||||||
|
const tomorrow = daysFromNow(1);
|
||||||
|
insertTvShow("tv-1", 100, 1, 2, {
|
||||||
|
title: "Breaking Bad",
|
||||||
|
airDates: [tomorrow, tomorrow],
|
||||||
|
});
|
||||||
|
insertStatus("user-1", "tv-1", "in_progress");
|
||||||
|
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 7 });
|
||||||
|
expect(result.items).toHaveLength(2);
|
||||||
|
expect(result.items[0].titleName).toBe("Breaking Bad");
|
||||||
|
expect(result.items[0].titleType).toBe("tv");
|
||||||
|
expect(result.items[0].date).toBe(tomorrow);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns upcoming movies for tracked titles", () => {
|
||||||
|
const nextWeek = daysFromNow(5);
|
||||||
|
insertTitle({ id: "m1", tmdbId: 1, type: "movie", title: "Dune 3", releaseDate: nextWeek });
|
||||||
|
insertStatus("user-1", "m1", "watchlist");
|
||||||
|
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 7 });
|
||||||
|
expect(result.items).toHaveLength(1);
|
||||||
|
expect(result.items[0].titleName).toBe("Dune 3");
|
||||||
|
expect(result.items[0].titleType).toBe("movie");
|
||||||
|
expect(result.items[0].date).toBe(nextWeek);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("includes items airing today", () => {
|
||||||
|
const today = daysFromNow(0);
|
||||||
|
insertTvShow("tv-1", 100, 1, 1, { airDates: [today] });
|
||||||
|
insertStatus("user-1", "tv-1", "in_progress");
|
||||||
|
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 7 });
|
||||||
|
expect(result.items).toHaveLength(1);
|
||||||
|
expect(result.items[0].date).toBe(today);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("excludes items beyond the days window", () => {
|
||||||
|
const farFuture = daysFromNow(91);
|
||||||
|
insertTvShow("tv-1", 100, 1, 1, { airDates: [farFuture] });
|
||||||
|
insertStatus("user-1", "tv-1", "in_progress");
|
||||||
|
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 90 });
|
||||||
|
expect(result.items).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("excludes titles not in user's library", () => {
|
||||||
|
const tomorrow = daysFromNow(1);
|
||||||
|
insertTvShow("tv-1", 100, 1, 1, { airDates: [tomorrow] });
|
||||||
|
// No insertStatus — not tracked
|
||||||
|
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 7 });
|
||||||
|
expect(result.items).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns empty when no upcoming items", () => {
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 7 });
|
||||||
|
expect(result.items).toHaveLength(0);
|
||||||
|
expect(result.nextCursor).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Sorting ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("sorting", () => {
|
||||||
|
test("sorts by date ascending, then title name", () => {
|
||||||
|
const day1 = daysFromNow(1);
|
||||||
|
const day2 = daysFromNow(2);
|
||||||
|
|
||||||
|
insertTvShow("tv-z", 101, 1, 1, { title: "Zebra Show", airDates: [day1] });
|
||||||
|
insertTvShow("tv-a", 102, 1, 1, { title: "Alpha Show", airDates: [day2] });
|
||||||
|
insertTvShow("tv-b", 103, 1, 1, { title: "Alpha Show 2", airDates: [day1] });
|
||||||
|
insertStatus("user-1", "tv-z", "in_progress");
|
||||||
|
insertStatus("user-1", "tv-a", "in_progress");
|
||||||
|
insertStatus("user-1", "tv-b", "in_progress");
|
||||||
|
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 7 });
|
||||||
|
expect(result.items.map((i) => i.titleName)).toEqual([
|
||||||
|
"Alpha Show 2",
|
||||||
|
"Zebra Show",
|
||||||
|
"Alpha Show",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("merges movies and episodes in date order", () => {
|
||||||
|
const day1 = daysFromNow(1);
|
||||||
|
const day2 = daysFromNow(2);
|
||||||
|
|
||||||
|
insertTitle({ id: "m1", tmdbId: 1, type: "movie", title: "A Movie", releaseDate: day2 });
|
||||||
|
insertTvShow("tv-1", 100, 1, 1, { title: "A Show", airDates: [day1] });
|
||||||
|
insertStatus("user-1", "m1", "watchlist");
|
||||||
|
insertStatus("user-1", "tv-1", "in_progress");
|
||||||
|
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 7 });
|
||||||
|
expect(result.items[0].titleType).toBe("tv");
|
||||||
|
expect(result.items[1].titleType).toBe("movie");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Batch collapse ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("batch collapse", () => {
|
||||||
|
test("collapses 3+ same-day episodes from the same title", () => {
|
||||||
|
const tomorrow = daysFromNow(1);
|
||||||
|
insertTvShow("tv-1", 100, 1, 4, {
|
||||||
|
title: "Batch Show",
|
||||||
|
airDates: [tomorrow, tomorrow, tomorrow, tomorrow],
|
||||||
|
});
|
||||||
|
insertStatus("user-1", "tv-1", "in_progress");
|
||||||
|
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 7 });
|
||||||
|
expect(result.items).toHaveLength(1);
|
||||||
|
expect(result.items[0].episodeCount).toBe(4);
|
||||||
|
expect(result.items[0].episodeName).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not collapse fewer than 3 same-day episodes", () => {
|
||||||
|
const tomorrow = daysFromNow(1);
|
||||||
|
insertTvShow("tv-1", 100, 1, 2, {
|
||||||
|
title: "Small Drop",
|
||||||
|
airDates: [tomorrow, tomorrow],
|
||||||
|
});
|
||||||
|
insertStatus("user-1", "tv-1", "in_progress");
|
||||||
|
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 7 });
|
||||||
|
expect(result.items).toHaveLength(2);
|
||||||
|
expect(result.items[0].episodeCount).toBe(1);
|
||||||
|
expect(result.items[0].episodeName).toBe("S1E1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not collapse episodes on different dates", () => {
|
||||||
|
const day1 = daysFromNow(1);
|
||||||
|
const day2 = daysFromNow(2);
|
||||||
|
const day3 = daysFromNow(3);
|
||||||
|
insertTvShow("tv-1", 100, 1, 3, {
|
||||||
|
title: "Spread Show",
|
||||||
|
airDates: [day1, day2, day3],
|
||||||
|
});
|
||||||
|
insertStatus("user-1", "tv-1", "in_progress");
|
||||||
|
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 7 });
|
||||||
|
expect(result.items).toHaveLength(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Cursor pagination ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("cursor pagination", () => {
|
||||||
|
test("paginates with limit and returns nextCursor", () => {
|
||||||
|
const day1 = daysFromNow(1);
|
||||||
|
const day2 = daysFromNow(2);
|
||||||
|
const day3 = daysFromNow(3);
|
||||||
|
|
||||||
|
insertTvShow("tv-a", 101, 1, 1, { title: "Show A", airDates: [day1] });
|
||||||
|
insertTvShow("tv-b", 102, 1, 1, { title: "Show B", airDates: [day2] });
|
||||||
|
insertTvShow("tv-c", 103, 1, 1, { title: "Show C", airDates: [day3] });
|
||||||
|
insertStatus("user-1", "tv-a", "in_progress");
|
||||||
|
insertStatus("user-1", "tv-b", "in_progress");
|
||||||
|
insertStatus("user-1", "tv-c", "in_progress");
|
||||||
|
|
||||||
|
const page1 = getUpcomingFeed("user-1", { days: 7, limit: 2 });
|
||||||
|
expect(page1.items).toHaveLength(2);
|
||||||
|
expect(page1.items[0].titleName).toBe("Show A");
|
||||||
|
expect(page1.items[1].titleName).toBe("Show B");
|
||||||
|
expect(page1.nextCursor).not.toBeNull();
|
||||||
|
|
||||||
|
const page2 = getUpcomingFeed("user-1", { days: 7, limit: 2, cursor: page1.nextCursor! });
|
||||||
|
expect(page2.items).toHaveLength(1);
|
||||||
|
expect(page2.items[0].titleName).toBe("Show C");
|
||||||
|
expect(page2.nextCursor).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles invalid cursor gracefully", () => {
|
||||||
|
const tomorrow = daysFromNow(1);
|
||||||
|
insertTvShow("tv-1", 100, 1, 1, { airDates: [tomorrow] });
|
||||||
|
insertStatus("user-1", "tv-1", "in_progress");
|
||||||
|
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 7, cursor: "not-valid-base64!" });
|
||||||
|
expect(result.items).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── isNewSeason ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("isNewSeason", () => {
|
||||||
|
test("marks episode 1 as new season for caught_up shows", () => {
|
||||||
|
const yesterday = daysFromNow(-1);
|
||||||
|
const tomorrow = daysFromNow(1);
|
||||||
|
// 2 seasons, 1 ep each; S1E1 aired yesterday, S2E1 airs tomorrow
|
||||||
|
const { episodeIds } = insertTvShow("tv-1", 100, 2, 1, {
|
||||||
|
title: "Returning Show",
|
||||||
|
airDates: [yesterday, tomorrow],
|
||||||
|
status: "Returning Series",
|
||||||
|
});
|
||||||
|
insertStatus("user-1", "tv-1", "in_progress");
|
||||||
|
// Watch S1E1 (the only aired episode) → caught_up display status
|
||||||
|
insertEpisodeWatch("user-1", episodeIds[0]);
|
||||||
|
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 7 });
|
||||||
|
expect(result.items).toHaveLength(1);
|
||||||
|
expect(result.items[0].isNewSeason).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not mark non-episode-1 as new season", () => {
|
||||||
|
const yesterday = daysFromNow(-1);
|
||||||
|
const tomorrow = daysFromNow(1);
|
||||||
|
// 1 season, 3 episodes — ep 1 aired yesterday, ep 2 and 3 air tomorrow
|
||||||
|
const { episodeIds } = insertTvShow("tv-1", 100, 1, 3, {
|
||||||
|
title: "Mid Show",
|
||||||
|
airDates: [yesterday, tomorrow, tomorrow],
|
||||||
|
status: "Returning Series",
|
||||||
|
});
|
||||||
|
insertStatus("user-1", "tv-1", "in_progress");
|
||||||
|
// Watch ep 1 (only aired episode) → caught_up
|
||||||
|
insertEpisodeWatch("user-1", episodeIds[0]);
|
||||||
|
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 7 });
|
||||||
|
// episodes 2 and 3 air tomorrow (not collapsed since only 2)
|
||||||
|
expect(result.items.every((i) => i.isNewSeason === false)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not mark episode 1 as new season for watching shows", () => {
|
||||||
|
const yesterday = daysFromNow(-1);
|
||||||
|
const tomorrow = daysFromNow(1);
|
||||||
|
// 2 seasons, 1 ep each; S1E1 aired yesterday, S2E1 airs tomorrow
|
||||||
|
insertTvShow("tv-1", 100, 2, 1, {
|
||||||
|
airDates: [yesterday, tomorrow],
|
||||||
|
status: "Returning Series",
|
||||||
|
});
|
||||||
|
insertStatus("user-1", "tv-1", "in_progress");
|
||||||
|
// No episodes watched → display status is "watching", not "caught_up"
|
||||||
|
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 7 });
|
||||||
|
expect(result.items).toHaveLength(1);
|
||||||
|
expect(result.items[0].isNewSeason).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Streaming provider ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("streaming provider", () => {
|
||||||
|
test("attaches flatrate streaming provider", () => {
|
||||||
|
const tomorrow = daysFromNow(1);
|
||||||
|
insertTvShow("tv-1", 100, 1, 1, { airDates: [tomorrow] });
|
||||||
|
insertStatus("user-1", "tv-1", "in_progress");
|
||||||
|
insertAvailabilityOffer("tv-1", { providerName: "Netflix", providerId: 8 });
|
||||||
|
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 7 });
|
||||||
|
expect(result.items[0].streamingProvider).toEqual({
|
||||||
|
providerId: 8,
|
||||||
|
providerName: "Netflix",
|
||||||
|
logoPath: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null when no flatrate provider exists", () => {
|
||||||
|
const tomorrow = daysFromNow(1);
|
||||||
|
insertTvShow("tv-1", 100, 1, 1, { airDates: [tomorrow] });
|
||||||
|
insertStatus("user-1", "tv-1", "in_progress");
|
||||||
|
insertAvailabilityOffer("tv-1", { offerType: "rent" });
|
||||||
|
|
||||||
|
const result = getUpcomingFeed("user-1", { days: 7 });
|
||||||
|
expect(result.items[0].streamingProvider).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { and, desc, eq, inArray, sql } from "drizzle-orm";
|
import { and, asc, desc, eq, gte, inArray, lte, sql } from "drizzle-orm";
|
||||||
|
|
||||||
import { db } from "../client";
|
import { db } from "../client";
|
||||||
import {
|
import {
|
||||||
@@ -312,3 +312,76 @@ export function getRecommendationRowsForTitle(titleId: string) {
|
|||||||
export function getTitleByIdOrNull(titleId: string) {
|
export function getTitleByIdOrNull(titleId: string) {
|
||||||
return db.select().from(titles).where(eq(titles.id, titleId)).get() ?? null;
|
return db.select().from(titles).where(eq(titles.id, titleId)).get() ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Upcoming feed queries ──────────────────────────────────────────
|
||||||
|
|
||||||
|
export function getUpcomingEpisodes(userId: string, fromDate: string, toDate: string) {
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
episodeId: episodes.id,
|
||||||
|
titleId: titles.id,
|
||||||
|
titleName: titles.title,
|
||||||
|
posterPath: titles.posterPath,
|
||||||
|
posterThumbHash: titles.posterThumbHash,
|
||||||
|
seasonNumber: seasons.seasonNumber,
|
||||||
|
episodeNumber: episodes.episodeNumber,
|
||||||
|
episodeName: episodes.name,
|
||||||
|
airDate: episodes.airDate,
|
||||||
|
userStatus: userTitleStatus.status,
|
||||||
|
})
|
||||||
|
.from(episodes)
|
||||||
|
.innerJoin(seasons, eq(episodes.seasonId, seasons.id))
|
||||||
|
.innerJoin(titles, and(eq(seasons.titleId, titles.id), eq(titles.type, "tv")))
|
||||||
|
.innerJoin(
|
||||||
|
userTitleStatus,
|
||||||
|
and(eq(userTitleStatus.titleId, titles.id), eq(userTitleStatus.userId, userId)),
|
||||||
|
)
|
||||||
|
.where(and(gte(episodes.airDate, fromDate), lte(episodes.airDate, toDate)))
|
||||||
|
.orderBy(asc(episodes.airDate), asc(titles.title))
|
||||||
|
.all();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUpcomingMovies(userId: string, fromDate: string, toDate: string) {
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
titleId: titles.id,
|
||||||
|
titleName: titles.title,
|
||||||
|
posterPath: titles.posterPath,
|
||||||
|
posterThumbHash: titles.posterThumbHash,
|
||||||
|
releaseDate: titles.releaseDate,
|
||||||
|
userStatus: userTitleStatus.status,
|
||||||
|
})
|
||||||
|
.from(titles)
|
||||||
|
.innerJoin(
|
||||||
|
userTitleStatus,
|
||||||
|
and(eq(userTitleStatus.titleId, titles.id), eq(userTitleStatus.userId, userId)),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(titles.type, "movie"),
|
||||||
|
gte(titles.releaseDate, fromDate),
|
||||||
|
lte(titles.releaseDate, toDate),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(asc(titles.releaseDate), asc(titles.title))
|
||||||
|
.all();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAvailabilityByTitleIds(titleIds: string[]) {
|
||||||
|
if (titleIds.length === 0) return [];
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
titleId: availabilityOffers.titleId,
|
||||||
|
providerId: availabilityOffers.providerId,
|
||||||
|
providerName: availabilityOffers.providerName,
|
||||||
|
logoPath: availabilityOffers.logoPath,
|
||||||
|
})
|
||||||
|
.from(availabilityOffers)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(availabilityOffers.titleId, titleIds),
|
||||||
|
eq(availabilityOffers.offerType, "flatrate"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.all();
|
||||||
|
}
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ export const episodes = sqliteTable(
|
|||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
uniqueIndex("episodes_seasonId_episodeNumber").on(table.seasonId, table.episodeNumber),
|
uniqueIndex("episodes_seasonId_episodeNumber").on(table.seasonId, table.episodeNumber),
|
||||||
|
index("episodes_airDate").on(table.airDate),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ export function insertTitle(
|
|||||||
tvdbId?: number;
|
tvdbId?: number;
|
||||||
type?: "movie" | "tv";
|
type?: "movie" | "tv";
|
||||||
title?: string;
|
title?: string;
|
||||||
|
releaseDate?: string;
|
||||||
|
posterPath?: string;
|
||||||
|
status?: string;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const id = overrides.id ?? "title-1";
|
const id = overrides.id ?? "title-1";
|
||||||
@@ -74,14 +77,30 @@ export function insertTitle(
|
|||||||
tvdbId: overrides.tvdbId,
|
tvdbId: overrides.tvdbId,
|
||||||
type: overrides.type ?? "movie",
|
type: overrides.type ?? "movie",
|
||||||
title: overrides.title ?? "Test Movie",
|
title: overrides.title ?? "Test Movie",
|
||||||
|
releaseDate: overrides.releaseDate,
|
||||||
|
posterPath: overrides.posterPath,
|
||||||
|
status: overrides.status,
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function insertTvShow(titleId = "tv-1", tmdbId = 99999, seasonCount = 1, epsPerSeason = 3) {
|
export function insertTvShow(
|
||||||
insertTitle({ id: titleId, tmdbId, type: "tv", title: "Test Show" });
|
titleId = "tv-1",
|
||||||
|
tmdbId = 99999,
|
||||||
|
seasonCount = 1,
|
||||||
|
epsPerSeason = 3,
|
||||||
|
options: { title?: string; airDates?: string[]; status?: string } = {},
|
||||||
|
) {
|
||||||
|
insertTitle({
|
||||||
|
id: titleId,
|
||||||
|
tmdbId,
|
||||||
|
type: "tv",
|
||||||
|
title: options.title ?? "Test Show",
|
||||||
|
status: options.status,
|
||||||
|
});
|
||||||
const episodeIds: string[] = [];
|
const episodeIds: string[] = [];
|
||||||
|
let airDateIdx = 0;
|
||||||
for (let s = 1; s <= seasonCount; s++) {
|
for (let s = 1; s <= seasonCount; s++) {
|
||||||
const seasonId = `${titleId}-s${s}`;
|
const seasonId = `${titleId}-s${s}`;
|
||||||
testDb.insert(seasons).values({ id: seasonId, titleId, seasonNumber: s }).run();
|
testDb.insert(seasons).values({ id: seasonId, titleId, seasonNumber: s }).run();
|
||||||
@@ -94,9 +113,11 @@ export function insertTvShow(titleId = "tv-1", tmdbId = 99999, seasonCount = 1,
|
|||||||
seasonId,
|
seasonId,
|
||||||
episodeNumber: e,
|
episodeNumber: e,
|
||||||
name: `S${s}E${e}`,
|
name: `S${s}E${e}`,
|
||||||
|
airDate: options.airDates?.[airDateIdx],
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
episodeIds.push(epId);
|
episodeIds.push(epId);
|
||||||
|
airDateIdx++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { titleId, episodeIds };
|
return { titleId, episodeIds };
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./locales": "./src/locales.ts",
|
"./locales": "./src/locales.ts",
|
||||||
"./format": "./src/format.ts",
|
"./format": "./src/format.ts",
|
||||||
|
"./date-buckets": "./src/date-buckets.ts",
|
||||||
"./test-utils": "./src/test-utils.tsx"
|
"./test-utils": "./src/test-utils.tsx"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { msg } from "@lingui/core/macro";
|
||||||
|
|
||||||
|
import { i18n } from "./index";
|
||||||
|
|
||||||
|
export type DateBucket<T> = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
items: T[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatLocalDate(d: Date): string {
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToday(): string {
|
||||||
|
return formatLocalDate(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDays(dateStr: string, days: number): string {
|
||||||
|
const d = new Date(`${dateStr}T00:00:00`);
|
||||||
|
d.setDate(d.getDate() + days);
|
||||||
|
return formatLocalDate(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEndOfWeek(today: string): string {
|
||||||
|
return addDays(today, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMonthLabel(dateStr: string): string {
|
||||||
|
const d = new Date(`${dateStr}T00:00:00`);
|
||||||
|
return new Intl.DateTimeFormat(i18n.locale, { month: "long" }).format(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
type BucketKey = "today" | "tomorrow" | "this_week" | "next_week" | string;
|
||||||
|
|
||||||
|
function getBucketKey(dateStr: string, today: string): BucketKey {
|
||||||
|
if (dateStr === today) return "today";
|
||||||
|
const tomorrow = addDays(today, 1);
|
||||||
|
if (dateStr === tomorrow) return "tomorrow";
|
||||||
|
const endOfWeek = getEndOfWeek(today);
|
||||||
|
if (dateStr <= endOfWeek) return "this_week";
|
||||||
|
const endOfNextWeek = addDays(endOfWeek, 7);
|
||||||
|
if (dateStr <= endOfNextWeek) return "next_week";
|
||||||
|
return `month_${dateStr.slice(0, 7)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBucketLabel(key: BucketKey): string {
|
||||||
|
if (key === "today") return i18n._(msg`Today`);
|
||||||
|
if (key === "tomorrow") return i18n._(msg`Tomorrow`);
|
||||||
|
if (key === "this_week") return i18n._(msg`This Week`);
|
||||||
|
if (key === "next_week") return i18n._(msg`Next Week`);
|
||||||
|
if (key.startsWith("month_")) {
|
||||||
|
return getMonthLabel(`${key.slice(6)}-01`);
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function groupByDateBucket<T extends { date: string }>(items: T[]): DateBucket<T>[] {
|
||||||
|
const today = getToday();
|
||||||
|
const bucketMap = new Map<string, { label: string; items: T[] }>();
|
||||||
|
const bucketOrder: string[] = [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const key = getBucketKey(item.date, today);
|
||||||
|
let bucket = bucketMap.get(key);
|
||||||
|
if (!bucket) {
|
||||||
|
bucket = { label: getBucketLabel(key), items: [] };
|
||||||
|
bucketMap.set(key, bucket);
|
||||||
|
bucketOrder.push(key);
|
||||||
|
}
|
||||||
|
bucket.items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucketOrder.map((key) => {
|
||||||
|
const bucket = bucketMap.get(key)!;
|
||||||
|
return { key, label: bucket.label, items: bucket.items };
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -413,6 +413,7 @@ msgid "Catch up"
|
|||||||
msgstr "اللحاق"
|
msgstr "اللحاق"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Caught Up"
|
msgid "Caught Up"
|
||||||
@@ -514,6 +515,7 @@ msgstr "إغلاق"
|
|||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
#: apps/web/src/components/dashboard/stats-display.tsx
|
#: apps/web/src/components/dashboard/stats-display.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Completed"
|
msgid "Completed"
|
||||||
@@ -838,6 +840,10 @@ msgstr "حلقات {period}"
|
|||||||
msgid "Episodes {select}"
|
msgid "Episodes {select}"
|
||||||
msgstr "حلقات {select}"
|
msgstr "حلقات {select}"
|
||||||
|
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Episodes and movies coming up in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#~ msgid "Episodes this week"
|
#~ msgid "Episodes this week"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -1321,6 +1327,7 @@ msgstr "تحديد الكل كمشاهدة"
|
|||||||
#~ msgid "Mark as Completed"
|
#~ msgid "Mark as Completed"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Mark as Watched"
|
msgid "Mark as Watched"
|
||||||
msgstr "تحديد كمشاهدة"
|
msgstr "تحديد كمشاهدة"
|
||||||
@@ -1329,6 +1336,10 @@ msgstr "تحديد كمشاهدة"
|
|||||||
#~ msgid "Mark as Watching"
|
#~ msgid "Mark as Watching"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
msgid "Mark Episode Watched"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/title/[id].tsx
|
#: apps/native/src/app/title/[id].tsx
|
||||||
#: apps/web/src/components/titles/title-actions.tsx
|
#: apps/web/src/components/titles/title-actions.tsx
|
||||||
msgid "Mark Watched"
|
msgid "Mark Watched"
|
||||||
@@ -1439,6 +1450,10 @@ msgstr "أبداً"
|
|||||||
msgid "Never run"
|
msgid "Never run"
|
||||||
msgstr "لم يُشغَّل قط"
|
msgstr "لم يُشغَّل قط"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#~ msgid "New"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "New account creation is currently disabled."
|
msgid "New account creation is currently disabled."
|
||||||
msgstr "إنشاء الحسابات الجديدة معطّل حالياً."
|
msgstr "إنشاء الحسابات الجديدة معطّل حالياً."
|
||||||
@@ -1460,6 +1475,11 @@ msgstr "كلمة المرور الجديدة"
|
|||||||
msgid "New password must be at least 8 characters"
|
msgid "New password must be at least 8 characters"
|
||||||
msgstr "يجب أن تتكون كلمة المرور الجديدة من 8 أحرف على الأقل"
|
msgstr "يجب أن تتكون كلمة المرور الجديدة من 8 أحرف على الأقل"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "New Season"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
@@ -1501,6 +1521,11 @@ msgstr "لم يتم العثور على نتائج."
|
|||||||
msgid "No titles found for this genre."
|
msgid "No titles found for this genre."
|
||||||
msgstr "لم يتم العثور على عناوين لهذا النوع."
|
msgstr "لم يتم العثور على عناوين لهذا النوع."
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "No upcoming episodes or releases in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/components/settings/integration-card.tsx
|
#: apps/native/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/integration-card.tsx
|
#: apps/web/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/system-health-section.tsx
|
#: apps/web/src/components/settings/system-health-section.tsx
|
||||||
@@ -1511,6 +1536,7 @@ msgstr "غير مُهيَّأ"
|
|||||||
msgid "Not Found"
|
msgid "Not Found"
|
||||||
msgstr "غير موجود"
|
msgstr "غير موجود"
|
||||||
|
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
msgid "On Watchlist"
|
msgid "On Watchlist"
|
||||||
msgstr "في قائمة المشاهدة"
|
msgstr "في قائمة المشاهدة"
|
||||||
@@ -1839,6 +1865,7 @@ msgid "Remove from library"
|
|||||||
msgstr "إزالة من المكتبة"
|
msgstr "إزالة من المكتبة"
|
||||||
|
|
||||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Remove from Library"
|
msgid "Remove from Library"
|
||||||
msgstr "إزالة من المكتبة"
|
msgstr "إزالة من المكتبة"
|
||||||
@@ -1938,6 +1965,16 @@ msgstr "راجع ما تم العثور عليه واختر ما تريد است
|
|||||||
msgid "Run now"
|
msgid "Run now"
|
||||||
msgstr "تشغيل الآن"
|
msgstr "تشغيل الآن"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "S{seasonNum} · {epCount, plural, one {# episode} other {# episodes}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||||
msgid "Saturday"
|
msgid "Saturday"
|
||||||
msgstr "السبت"
|
msgstr "السبت"
|
||||||
@@ -2033,6 +2070,11 @@ msgstr "المواسم"
|
|||||||
msgid "Security"
|
msgid "Security"
|
||||||
msgstr "الأمان"
|
msgstr "الأمان"
|
||||||
|
|
||||||
|
#: apps/native/src/components/ui/section-header.tsx
|
||||||
|
#: apps/web/src/components/dashboard/feed-section.tsx
|
||||||
|
msgid "See all"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/landing-page.tsx
|
#: apps/web/src/components/landing-page.tsx
|
||||||
msgid "Self-hosted movie & TV tracker"
|
msgid "Self-hosted movie & TV tracker"
|
||||||
msgstr "تطبيق تتبع الأفلام والمسلسلات ذاتي الاستضافة"
|
msgstr "تطبيق تتبع الأفلام والمسلسلات ذاتي الاستضافة"
|
||||||
@@ -2453,6 +2495,15 @@ msgstr "تم إلغاء مشاهدة م{seasonNum} ح{epNum}"
|
|||||||
msgid "Up next"
|
msgid "Up next"
|
||||||
msgstr "التالي"
|
msgstr "التالي"
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Upcoming"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||||
#~ msgid "Update available: {0}"
|
#~ msgid "Update available: {0}"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -2571,6 +2622,7 @@ msgid "Watched S{seasonNum} E{epNum}"
|
|||||||
msgstr "تمت مشاهدة م{seasonNum} ح{epNum}"
|
msgstr "تمت مشاهدة م{seasonNum} ح{epNum}"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Watching"
|
msgid "Watching"
|
||||||
@@ -2645,4 +2697,3 @@ msgstr "مكتبتك فارغة"
|
|||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "Your name"
|
msgid "Your name"
|
||||||
msgstr "اسمك"
|
msgstr "اسمك"
|
||||||
|
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ msgid "Catch up"
|
|||||||
msgstr "Aufholen"
|
msgstr "Aufholen"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Caught Up"
|
msgid "Caught Up"
|
||||||
@@ -514,6 +515,7 @@ msgstr "Schließen"
|
|||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
#: apps/web/src/components/dashboard/stats-display.tsx
|
#: apps/web/src/components/dashboard/stats-display.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Completed"
|
msgid "Completed"
|
||||||
@@ -838,6 +840,10 @@ msgstr ""
|
|||||||
msgid "Episodes {select}"
|
msgid "Episodes {select}"
|
||||||
msgstr "Episoden {select}"
|
msgstr "Episoden {select}"
|
||||||
|
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Episodes and movies coming up in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#~ msgid "Episodes this week"
|
#~ msgid "Episodes this week"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -1321,6 +1327,7 @@ msgstr "Alle als gesehen markieren"
|
|||||||
#~ msgid "Mark as Completed"
|
#~ msgid "Mark as Completed"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Mark as Watched"
|
msgid "Mark as Watched"
|
||||||
msgstr "Als gesehen markieren"
|
msgstr "Als gesehen markieren"
|
||||||
@@ -1329,6 +1336,10 @@ msgstr "Als gesehen markieren"
|
|||||||
#~ msgid "Mark as Watching"
|
#~ msgid "Mark as Watching"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
msgid "Mark Episode Watched"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/title/[id].tsx
|
#: apps/native/src/app/title/[id].tsx
|
||||||
#: apps/web/src/components/titles/title-actions.tsx
|
#: apps/web/src/components/titles/title-actions.tsx
|
||||||
msgid "Mark Watched"
|
msgid "Mark Watched"
|
||||||
@@ -1439,6 +1450,10 @@ msgstr "Nie"
|
|||||||
msgid "Never run"
|
msgid "Never run"
|
||||||
msgstr "Noch nie ausgeführt"
|
msgstr "Noch nie ausgeführt"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#~ msgid "New"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "New account creation is currently disabled."
|
msgid "New account creation is currently disabled."
|
||||||
msgstr "Das Erstellen neuer Konten ist derzeit deaktiviert."
|
msgstr "Das Erstellen neuer Konten ist derzeit deaktiviert."
|
||||||
@@ -1460,6 +1475,11 @@ msgstr "Neues Passwort"
|
|||||||
msgid "New password must be at least 8 characters"
|
msgid "New password must be at least 8 characters"
|
||||||
msgstr "Das neue Passwort muss mindestens 8 Zeichen lang sein"
|
msgstr "Das neue Passwort muss mindestens 8 Zeichen lang sein"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "New Season"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
@@ -1501,6 +1521,11 @@ msgstr "Keine Ergebnisse gefunden."
|
|||||||
msgid "No titles found for this genre."
|
msgid "No titles found for this genre."
|
||||||
msgstr "Keine Titel für dieses Genre gefunden."
|
msgstr "Keine Titel für dieses Genre gefunden."
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "No upcoming episodes or releases in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/components/settings/integration-card.tsx
|
#: apps/native/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/integration-card.tsx
|
#: apps/web/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/system-health-section.tsx
|
#: apps/web/src/components/settings/system-health-section.tsx
|
||||||
@@ -1511,6 +1536,7 @@ msgstr "Nicht konfiguriert"
|
|||||||
msgid "Not Found"
|
msgid "Not Found"
|
||||||
msgstr "Nicht gefunden"
|
msgstr "Nicht gefunden"
|
||||||
|
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
msgid "On Watchlist"
|
msgid "On Watchlist"
|
||||||
msgstr "Auf der Merkliste"
|
msgstr "Auf der Merkliste"
|
||||||
@@ -1839,6 +1865,7 @@ msgid "Remove from library"
|
|||||||
msgstr "Aus Bibliothek entfernen"
|
msgstr "Aus Bibliothek entfernen"
|
||||||
|
|
||||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Remove from Library"
|
msgid "Remove from Library"
|
||||||
msgstr "Aus Bibliothek entfernen"
|
msgstr "Aus Bibliothek entfernen"
|
||||||
@@ -1938,6 +1965,16 @@ msgstr "Überprüfe die gefundenen Einträge und wähle aus, was importiert werd
|
|||||||
msgid "Run now"
|
msgid "Run now"
|
||||||
msgstr "Jetzt ausführen"
|
msgstr "Jetzt ausführen"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "S{seasonNum} · {epCount, plural, one {# episode} other {# episodes}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||||
msgid "Saturday"
|
msgid "Saturday"
|
||||||
msgstr "Samstag"
|
msgstr "Samstag"
|
||||||
@@ -2033,6 +2070,11 @@ msgstr "Staffeln"
|
|||||||
msgid "Security"
|
msgid "Security"
|
||||||
msgstr "Sicherheit"
|
msgstr "Sicherheit"
|
||||||
|
|
||||||
|
#: apps/native/src/components/ui/section-header.tsx
|
||||||
|
#: apps/web/src/components/dashboard/feed-section.tsx
|
||||||
|
msgid "See all"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/landing-page.tsx
|
#: apps/web/src/components/landing-page.tsx
|
||||||
msgid "Self-hosted movie & TV tracker"
|
msgid "Self-hosted movie & TV tracker"
|
||||||
msgstr "Selbstgehosteter Film- & Serien-Tracker"
|
msgstr "Selbstgehosteter Film- & Serien-Tracker"
|
||||||
@@ -2453,6 +2495,15 @@ msgstr "S{seasonNum} E{epNum} als ungesehen markiert"
|
|||||||
msgid "Up next"
|
msgid "Up next"
|
||||||
msgstr "Als Nächstes"
|
msgstr "Als Nächstes"
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Upcoming"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||||
#~ msgid "Update available: {0}"
|
#~ msgid "Update available: {0}"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -2571,6 +2622,7 @@ msgid "Watched S{seasonNum} E{epNum}"
|
|||||||
msgstr "S{seasonNum} E{epNum} als gesehen markiert"
|
msgstr "S{seasonNum} E{epNum} als gesehen markiert"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Watching"
|
msgid "Watching"
|
||||||
@@ -2645,4 +2697,3 @@ msgstr "Deine Bibliothek ist leer"
|
|||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "Your name"
|
msgid "Your name"
|
||||||
msgstr "Dein Name"
|
msgstr "Dein Name"
|
||||||
|
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ msgid "Catch up"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Caught Up"
|
msgid "Caught Up"
|
||||||
@@ -514,6 +515,7 @@ msgstr ""
|
|||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
#: apps/web/src/components/dashboard/stats-display.tsx
|
#: apps/web/src/components/dashboard/stats-display.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Completed"
|
msgid "Completed"
|
||||||
@@ -838,6 +840,10 @@ msgstr "Episodes {period}"
|
|||||||
msgid "Episodes {select}"
|
msgid "Episodes {select}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Episodes and movies coming up in the next 90 days."
|
||||||
|
msgstr "Episodes and movies coming up in the next 90 days."
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#~ msgid "Episodes this week"
|
#~ msgid "Episodes this week"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -1321,6 +1327,7 @@ msgstr ""
|
|||||||
#~ msgid "Mark as Completed"
|
#~ msgid "Mark as Completed"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Mark as Watched"
|
msgid "Mark as Watched"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1329,6 +1336,10 @@ msgstr ""
|
|||||||
#~ msgid "Mark as Watching"
|
#~ msgid "Mark as Watching"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
msgid "Mark Episode Watched"
|
||||||
|
msgstr "Mark Episode Watched"
|
||||||
|
|
||||||
#: apps/native/src/app/title/[id].tsx
|
#: apps/native/src/app/title/[id].tsx
|
||||||
#: apps/web/src/components/titles/title-actions.tsx
|
#: apps/web/src/components/titles/title-actions.tsx
|
||||||
msgid "Mark Watched"
|
msgid "Mark Watched"
|
||||||
@@ -1439,6 +1450,10 @@ msgstr ""
|
|||||||
msgid "Never run"
|
msgid "Never run"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#~ msgid "New"
|
||||||
|
#~ msgstr "New"
|
||||||
|
|
||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "New account creation is currently disabled."
|
msgid "New account creation is currently disabled."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1460,6 +1475,11 @@ msgstr ""
|
|||||||
msgid "New password must be at least 8 characters"
|
msgid "New password must be at least 8 characters"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "New Season"
|
||||||
|
msgstr "New Season"
|
||||||
|
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
@@ -1501,6 +1521,11 @@ msgstr ""
|
|||||||
msgid "No titles found for this genre."
|
msgid "No titles found for this genre."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "No upcoming episodes or releases in the next 90 days."
|
||||||
|
msgstr "No upcoming episodes or releases in the next 90 days."
|
||||||
|
|
||||||
#: apps/native/src/components/settings/integration-card.tsx
|
#: apps/native/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/integration-card.tsx
|
#: apps/web/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/system-health-section.tsx
|
#: apps/web/src/components/settings/system-health-section.tsx
|
||||||
@@ -1511,6 +1536,7 @@ msgstr ""
|
|||||||
msgid "Not Found"
|
msgid "Not Found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
msgid "On Watchlist"
|
msgid "On Watchlist"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1839,6 +1865,7 @@ msgid "Remove from library"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Remove from Library"
|
msgid "Remove from Library"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1938,6 +1965,16 @@ msgstr ""
|
|||||||
msgid "Run now"
|
msgid "Run now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
|
||||||
|
#~ msgstr "S{0} · {1} {2, plural, one {episode} other {episodes}}"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "S{seasonNum} · {epCount, plural, one {# episode} other {# episodes}}"
|
||||||
|
msgstr "S{seasonNum} · {epCount, plural, one {# episode} other {# episodes}}"
|
||||||
|
|
||||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||||
msgid "Saturday"
|
msgid "Saturday"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2033,6 +2070,11 @@ msgstr ""
|
|||||||
msgid "Security"
|
msgid "Security"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/ui/section-header.tsx
|
||||||
|
#: apps/web/src/components/dashboard/feed-section.tsx
|
||||||
|
msgid "See all"
|
||||||
|
msgstr "See all"
|
||||||
|
|
||||||
#: apps/web/src/components/landing-page.tsx
|
#: apps/web/src/components/landing-page.tsx
|
||||||
msgid "Self-hosted movie & TV tracker"
|
msgid "Self-hosted movie & TV tracker"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2453,6 +2495,15 @@ msgstr ""
|
|||||||
msgid "Up next"
|
msgid "Up next"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Upcoming"
|
||||||
|
msgstr "Upcoming"
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||||
#~ msgid "Update available: {0}"
|
#~ msgid "Update available: {0}"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -2571,6 +2622,7 @@ msgid "Watched S{seasonNum} E{epNum}"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Watching"
|
msgid "Watching"
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ msgid "Catch up"
|
|||||||
msgstr "Ponerse al día"
|
msgstr "Ponerse al día"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Caught Up"
|
msgid "Caught Up"
|
||||||
@@ -514,6 +515,7 @@ msgstr "Cerrar"
|
|||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
#: apps/web/src/components/dashboard/stats-display.tsx
|
#: apps/web/src/components/dashboard/stats-display.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Completed"
|
msgid "Completed"
|
||||||
@@ -838,6 +840,10 @@ msgstr ""
|
|||||||
msgid "Episodes {select}"
|
msgid "Episodes {select}"
|
||||||
msgstr "Episodios {select}"
|
msgstr "Episodios {select}"
|
||||||
|
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Episodes and movies coming up in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#~ msgid "Episodes this week"
|
#~ msgid "Episodes this week"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -1321,6 +1327,7 @@ msgstr "Marcar todos como vistos"
|
|||||||
#~ msgid "Mark as Completed"
|
#~ msgid "Mark as Completed"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Mark as Watched"
|
msgid "Mark as Watched"
|
||||||
msgstr "Marcar como visto"
|
msgstr "Marcar como visto"
|
||||||
@@ -1329,6 +1336,10 @@ msgstr "Marcar como visto"
|
|||||||
#~ msgid "Mark as Watching"
|
#~ msgid "Mark as Watching"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
msgid "Mark Episode Watched"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/title/[id].tsx
|
#: apps/native/src/app/title/[id].tsx
|
||||||
#: apps/web/src/components/titles/title-actions.tsx
|
#: apps/web/src/components/titles/title-actions.tsx
|
||||||
msgid "Mark Watched"
|
msgid "Mark Watched"
|
||||||
@@ -1439,6 +1450,10 @@ msgstr "Nunca"
|
|||||||
msgid "Never run"
|
msgid "Never run"
|
||||||
msgstr "Nunca ejecutado"
|
msgstr "Nunca ejecutado"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#~ msgid "New"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "New account creation is currently disabled."
|
msgid "New account creation is currently disabled."
|
||||||
msgstr "La creación de nuevas cuentas está desactivada actualmente."
|
msgstr "La creación de nuevas cuentas está desactivada actualmente."
|
||||||
@@ -1460,6 +1475,11 @@ msgstr "Nueva contraseña"
|
|||||||
msgid "New password must be at least 8 characters"
|
msgid "New password must be at least 8 characters"
|
||||||
msgstr "La nueva contraseña debe tener al menos 8 caracteres"
|
msgstr "La nueva contraseña debe tener al menos 8 caracteres"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "New Season"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
@@ -1501,6 +1521,11 @@ msgstr "No se encontraron resultados."
|
|||||||
msgid "No titles found for this genre."
|
msgid "No titles found for this genre."
|
||||||
msgstr "No se encontraron títulos para este género."
|
msgstr "No se encontraron títulos para este género."
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "No upcoming episodes or releases in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/components/settings/integration-card.tsx
|
#: apps/native/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/integration-card.tsx
|
#: apps/web/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/system-health-section.tsx
|
#: apps/web/src/components/settings/system-health-section.tsx
|
||||||
@@ -1511,6 +1536,7 @@ msgstr "No configurado"
|
|||||||
msgid "Not Found"
|
msgid "Not Found"
|
||||||
msgstr "No encontrado"
|
msgstr "No encontrado"
|
||||||
|
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
msgid "On Watchlist"
|
msgid "On Watchlist"
|
||||||
msgstr "En la lista"
|
msgstr "En la lista"
|
||||||
@@ -1839,6 +1865,7 @@ msgid "Remove from library"
|
|||||||
msgstr "Eliminar de la biblioteca"
|
msgstr "Eliminar de la biblioteca"
|
||||||
|
|
||||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Remove from Library"
|
msgid "Remove from Library"
|
||||||
msgstr "Eliminar de la biblioteca"
|
msgstr "Eliminar de la biblioteca"
|
||||||
@@ -1938,6 +1965,16 @@ msgstr "Revisa lo encontrado y elige qué importar."
|
|||||||
msgid "Run now"
|
msgid "Run now"
|
||||||
msgstr "Ejecutar ahora"
|
msgstr "Ejecutar ahora"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "S{seasonNum} · {epCount, plural, one {# episode} other {# episodes}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||||
msgid "Saturday"
|
msgid "Saturday"
|
||||||
msgstr "Sábado"
|
msgstr "Sábado"
|
||||||
@@ -2033,6 +2070,11 @@ msgstr "Temporadas"
|
|||||||
msgid "Security"
|
msgid "Security"
|
||||||
msgstr "Seguridad"
|
msgstr "Seguridad"
|
||||||
|
|
||||||
|
#: apps/native/src/components/ui/section-header.tsx
|
||||||
|
#: apps/web/src/components/dashboard/feed-section.tsx
|
||||||
|
msgid "See all"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/landing-page.tsx
|
#: apps/web/src/components/landing-page.tsx
|
||||||
msgid "Self-hosted movie & TV tracker"
|
msgid "Self-hosted movie & TV tracker"
|
||||||
msgstr "Seguimiento de películas y series autoalojado"
|
msgstr "Seguimiento de películas y series autoalojado"
|
||||||
@@ -2453,6 +2495,15 @@ msgstr "No visto T{seasonNum} E{epNum}"
|
|||||||
msgid "Up next"
|
msgid "Up next"
|
||||||
msgstr "A continuación"
|
msgstr "A continuación"
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Upcoming"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||||
#~ msgid "Update available: {0}"
|
#~ msgid "Update available: {0}"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -2571,6 +2622,7 @@ msgid "Watched S{seasonNum} E{epNum}"
|
|||||||
msgstr "Visto T{seasonNum} E{epNum}"
|
msgstr "Visto T{seasonNum} E{epNum}"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Watching"
|
msgid "Watching"
|
||||||
@@ -2645,4 +2697,3 @@ msgstr "Tu biblioteca está vacía"
|
|||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "Your name"
|
msgid "Your name"
|
||||||
msgstr "Tu nombre"
|
msgstr "Tu nombre"
|
||||||
|
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ msgid "Catch up"
|
|||||||
msgstr "Rattraper"
|
msgstr "Rattraper"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Caught Up"
|
msgid "Caught Up"
|
||||||
@@ -514,6 +515,7 @@ msgstr "Fermer"
|
|||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
#: apps/web/src/components/dashboard/stats-display.tsx
|
#: apps/web/src/components/dashboard/stats-display.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Completed"
|
msgid "Completed"
|
||||||
@@ -838,6 +840,10 @@ msgstr ""
|
|||||||
msgid "Episodes {select}"
|
msgid "Episodes {select}"
|
||||||
msgstr "Épisodes {select}"
|
msgstr "Épisodes {select}"
|
||||||
|
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Episodes and movies coming up in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#~ msgid "Episodes this week"
|
#~ msgid "Episodes this week"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -1321,6 +1327,7 @@ msgstr "Tout marquer comme visionné"
|
|||||||
#~ msgid "Mark as Completed"
|
#~ msgid "Mark as Completed"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Mark as Watched"
|
msgid "Mark as Watched"
|
||||||
msgstr "Marquer comme visionné"
|
msgstr "Marquer comme visionné"
|
||||||
@@ -1329,6 +1336,10 @@ msgstr "Marquer comme visionné"
|
|||||||
#~ msgid "Mark as Watching"
|
#~ msgid "Mark as Watching"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
msgid "Mark Episode Watched"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/title/[id].tsx
|
#: apps/native/src/app/title/[id].tsx
|
||||||
#: apps/web/src/components/titles/title-actions.tsx
|
#: apps/web/src/components/titles/title-actions.tsx
|
||||||
msgid "Mark Watched"
|
msgid "Mark Watched"
|
||||||
@@ -1439,6 +1450,10 @@ msgstr "Jamais"
|
|||||||
msgid "Never run"
|
msgid "Never run"
|
||||||
msgstr "Jamais exécuté"
|
msgstr "Jamais exécuté"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#~ msgid "New"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "New account creation is currently disabled."
|
msgid "New account creation is currently disabled."
|
||||||
msgstr "La création de nouveaux comptes est actuellement désactivée."
|
msgstr "La création de nouveaux comptes est actuellement désactivée."
|
||||||
@@ -1460,6 +1475,11 @@ msgstr "Nouveau mot de passe"
|
|||||||
msgid "New password must be at least 8 characters"
|
msgid "New password must be at least 8 characters"
|
||||||
msgstr "Le nouveau mot de passe doit comporter au moins 8 caractères"
|
msgstr "Le nouveau mot de passe doit comporter au moins 8 caractères"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "New Season"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
@@ -1501,6 +1521,11 @@ msgstr "Aucun résultat trouvé."
|
|||||||
msgid "No titles found for this genre."
|
msgid "No titles found for this genre."
|
||||||
msgstr "Aucun titre trouvé pour ce genre."
|
msgstr "Aucun titre trouvé pour ce genre."
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "No upcoming episodes or releases in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/components/settings/integration-card.tsx
|
#: apps/native/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/integration-card.tsx
|
#: apps/web/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/system-health-section.tsx
|
#: apps/web/src/components/settings/system-health-section.tsx
|
||||||
@@ -1511,6 +1536,7 @@ msgstr "Non configuré"
|
|||||||
msgid "Not Found"
|
msgid "Not Found"
|
||||||
msgstr "Introuvable"
|
msgstr "Introuvable"
|
||||||
|
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
msgid "On Watchlist"
|
msgid "On Watchlist"
|
||||||
msgstr "Dans la liste de suivi"
|
msgstr "Dans la liste de suivi"
|
||||||
@@ -1839,6 +1865,7 @@ msgid "Remove from library"
|
|||||||
msgstr "Supprimer de la bibliothèque"
|
msgstr "Supprimer de la bibliothèque"
|
||||||
|
|
||||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Remove from Library"
|
msgid "Remove from Library"
|
||||||
msgstr "Supprimer de la bibliothèque"
|
msgstr "Supprimer de la bibliothèque"
|
||||||
@@ -1938,6 +1965,16 @@ msgstr "Vérifiez ce qui a été trouvé et choisissez ce que vous souhaitez imp
|
|||||||
msgid "Run now"
|
msgid "Run now"
|
||||||
msgstr "Exécuter maintenant"
|
msgstr "Exécuter maintenant"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "S{seasonNum} · {epCount, plural, one {# episode} other {# episodes}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||||
msgid "Saturday"
|
msgid "Saturday"
|
||||||
msgstr "Samedi"
|
msgstr "Samedi"
|
||||||
@@ -2033,6 +2070,11 @@ msgstr "Saisons"
|
|||||||
msgid "Security"
|
msgid "Security"
|
||||||
msgstr "Sécurité"
|
msgstr "Sécurité"
|
||||||
|
|
||||||
|
#: apps/native/src/components/ui/section-header.tsx
|
||||||
|
#: apps/web/src/components/dashboard/feed-section.tsx
|
||||||
|
msgid "See all"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/landing-page.tsx
|
#: apps/web/src/components/landing-page.tsx
|
||||||
msgid "Self-hosted movie & TV tracker"
|
msgid "Self-hosted movie & TV tracker"
|
||||||
msgstr "Suivi de films et séries auto-hébergé"
|
msgstr "Suivi de films et séries auto-hébergé"
|
||||||
@@ -2453,6 +2495,15 @@ msgstr "Démarqué S{seasonNum} E{epNum}"
|
|||||||
msgid "Up next"
|
msgid "Up next"
|
||||||
msgstr "Suivant"
|
msgstr "Suivant"
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Upcoming"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||||
#~ msgid "Update available: {0}"
|
#~ msgid "Update available: {0}"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -2571,6 +2622,7 @@ msgid "Watched S{seasonNum} E{epNum}"
|
|||||||
msgstr "Visionné S{seasonNum} E{epNum}"
|
msgstr "Visionné S{seasonNum} E{epNum}"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Watching"
|
msgid "Watching"
|
||||||
@@ -2645,4 +2697,3 @@ msgstr "Votre bibliothèque est vide"
|
|||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "Your name"
|
msgid "Your name"
|
||||||
msgstr "Votre nom"
|
msgstr "Votre nom"
|
||||||
|
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ msgid "Catch up"
|
|||||||
msgstr "השלם צפייה"
|
msgstr "השלם צפייה"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Caught Up"
|
msgid "Caught Up"
|
||||||
@@ -514,6 +515,7 @@ msgstr "סגור"
|
|||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
#: apps/web/src/components/dashboard/stats-display.tsx
|
#: apps/web/src/components/dashboard/stats-display.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Completed"
|
msgid "Completed"
|
||||||
@@ -838,6 +840,10 @@ msgstr "פרקים {period}"
|
|||||||
msgid "Episodes {select}"
|
msgid "Episodes {select}"
|
||||||
msgstr "פרקים {select}"
|
msgstr "פרקים {select}"
|
||||||
|
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Episodes and movies coming up in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#~ msgid "Episodes this week"
|
#~ msgid "Episodes this week"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -1321,6 +1327,7 @@ msgstr "סמן הכל כנצפה"
|
|||||||
#~ msgid "Mark as Completed"
|
#~ msgid "Mark as Completed"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Mark as Watched"
|
msgid "Mark as Watched"
|
||||||
msgstr "סמן כנצפה"
|
msgstr "סמן כנצפה"
|
||||||
@@ -1329,6 +1336,10 @@ msgstr "סמן כנצפה"
|
|||||||
#~ msgid "Mark as Watching"
|
#~ msgid "Mark as Watching"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
msgid "Mark Episode Watched"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/title/[id].tsx
|
#: apps/native/src/app/title/[id].tsx
|
||||||
#: apps/web/src/components/titles/title-actions.tsx
|
#: apps/web/src/components/titles/title-actions.tsx
|
||||||
msgid "Mark Watched"
|
msgid "Mark Watched"
|
||||||
@@ -1439,6 +1450,10 @@ msgstr "אף פעם"
|
|||||||
msgid "Never run"
|
msgid "Never run"
|
||||||
msgstr "מעולם לא הורץ"
|
msgstr "מעולם לא הורץ"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#~ msgid "New"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "New account creation is currently disabled."
|
msgid "New account creation is currently disabled."
|
||||||
msgstr "יצירת חשבון חדש כרגע מושבתת."
|
msgstr "יצירת חשבון חדש כרגע מושבתת."
|
||||||
@@ -1460,6 +1475,11 @@ msgstr "סיסמה חדשה"
|
|||||||
msgid "New password must be at least 8 characters"
|
msgid "New password must be at least 8 characters"
|
||||||
msgstr "הסיסמה החדשה חייבת להכיל לפחות 8 תווים"
|
msgstr "הסיסמה החדשה חייבת להכיל לפחות 8 תווים"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "New Season"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
@@ -1501,6 +1521,11 @@ msgstr "לא נמצאו תוצאות."
|
|||||||
msgid "No titles found for this genre."
|
msgid "No titles found for this genre."
|
||||||
msgstr "לא נמצאו כותרים לז'אנר זה."
|
msgstr "לא נמצאו כותרים לז'אנר זה."
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "No upcoming episodes or releases in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/components/settings/integration-card.tsx
|
#: apps/native/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/integration-card.tsx
|
#: apps/web/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/system-health-section.tsx
|
#: apps/web/src/components/settings/system-health-section.tsx
|
||||||
@@ -1511,6 +1536,7 @@ msgstr "לא מוגדר"
|
|||||||
msgid "Not Found"
|
msgid "Not Found"
|
||||||
msgstr "לא נמצא"
|
msgstr "לא נמצא"
|
||||||
|
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
msgid "On Watchlist"
|
msgid "On Watchlist"
|
||||||
msgstr "ברשימת הצפייה"
|
msgstr "ברשימת הצפייה"
|
||||||
@@ -1839,6 +1865,7 @@ msgid "Remove from library"
|
|||||||
msgstr "הסר מהספרייה"
|
msgstr "הסר מהספרייה"
|
||||||
|
|
||||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Remove from Library"
|
msgid "Remove from Library"
|
||||||
msgstr "הסר מהספרייה"
|
msgstr "הסר מהספרייה"
|
||||||
@@ -1938,6 +1965,16 @@ msgstr "בדוק מה נמצא ובחר מה לייבא."
|
|||||||
msgid "Run now"
|
msgid "Run now"
|
||||||
msgstr "הפעל עכשיו"
|
msgstr "הפעל עכשיו"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "S{seasonNum} · {epCount, plural, one {# episode} other {# episodes}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||||
msgid "Saturday"
|
msgid "Saturday"
|
||||||
msgstr "שבת"
|
msgstr "שבת"
|
||||||
@@ -2033,6 +2070,11 @@ msgstr "עונות"
|
|||||||
msgid "Security"
|
msgid "Security"
|
||||||
msgstr "אבטחה"
|
msgstr "אבטחה"
|
||||||
|
|
||||||
|
#: apps/native/src/components/ui/section-header.tsx
|
||||||
|
#: apps/web/src/components/dashboard/feed-section.tsx
|
||||||
|
msgid "See all"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/landing-page.tsx
|
#: apps/web/src/components/landing-page.tsx
|
||||||
msgid "Self-hosted movie & TV tracker"
|
msgid "Self-hosted movie & TV tracker"
|
||||||
msgstr "מעקב סרטים וטלוויזיה לאירוח עצמי"
|
msgstr "מעקב סרטים וטלוויזיה לאירוח עצמי"
|
||||||
@@ -2453,6 +2495,15 @@ msgstr "בוטלה צפייה ב-S{seasonNum} E{epNum}"
|
|||||||
msgid "Up next"
|
msgid "Up next"
|
||||||
msgstr "הבא בתור"
|
msgstr "הבא בתור"
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Upcoming"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||||
#~ msgid "Update available: {0}"
|
#~ msgid "Update available: {0}"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -2571,6 +2622,7 @@ msgid "Watched S{seasonNum} E{epNum}"
|
|||||||
msgstr "נצפה S{seasonNum} E{epNum}"
|
msgstr "נצפה S{seasonNum} E{epNum}"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Watching"
|
msgid "Watching"
|
||||||
@@ -2645,4 +2697,3 @@ msgstr "הספרייה שלך ריקה"
|
|||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "Your name"
|
msgid "Your name"
|
||||||
msgstr "שמך"
|
msgstr "שמך"
|
||||||
|
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ msgid "Catch up"
|
|||||||
msgstr "Recupera episodi"
|
msgstr "Recupera episodi"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Caught Up"
|
msgid "Caught Up"
|
||||||
@@ -514,6 +515,7 @@ msgstr "Chiudi"
|
|||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
#: apps/web/src/components/dashboard/stats-display.tsx
|
#: apps/web/src/components/dashboard/stats-display.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Completed"
|
msgid "Completed"
|
||||||
@@ -838,6 +840,10 @@ msgstr ""
|
|||||||
msgid "Episodes {select}"
|
msgid "Episodes {select}"
|
||||||
msgstr "Episodi {select}"
|
msgstr "Episodi {select}"
|
||||||
|
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Episodes and movies coming up in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#~ msgid "Episodes this week"
|
#~ msgid "Episodes this week"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -1321,6 +1327,7 @@ msgstr "Segna tutti come visti"
|
|||||||
#~ msgid "Mark as Completed"
|
#~ msgid "Mark as Completed"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Mark as Watched"
|
msgid "Mark as Watched"
|
||||||
msgstr "Segna come visto"
|
msgstr "Segna come visto"
|
||||||
@@ -1329,6 +1336,10 @@ msgstr "Segna come visto"
|
|||||||
#~ msgid "Mark as Watching"
|
#~ msgid "Mark as Watching"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
msgid "Mark Episode Watched"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/title/[id].tsx
|
#: apps/native/src/app/title/[id].tsx
|
||||||
#: apps/web/src/components/titles/title-actions.tsx
|
#: apps/web/src/components/titles/title-actions.tsx
|
||||||
msgid "Mark Watched"
|
msgid "Mark Watched"
|
||||||
@@ -1439,6 +1450,10 @@ msgstr "Mai"
|
|||||||
msgid "Never run"
|
msgid "Never run"
|
||||||
msgstr "Mai eseguito"
|
msgstr "Mai eseguito"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#~ msgid "New"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "New account creation is currently disabled."
|
msgid "New account creation is currently disabled."
|
||||||
msgstr "La creazione di nuovi account è attualmente disabilitata."
|
msgstr "La creazione di nuovi account è attualmente disabilitata."
|
||||||
@@ -1460,6 +1475,11 @@ msgstr "Nuova password"
|
|||||||
msgid "New password must be at least 8 characters"
|
msgid "New password must be at least 8 characters"
|
||||||
msgstr "La nuova password deve contenere almeno 8 caratteri"
|
msgstr "La nuova password deve contenere almeno 8 caratteri"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "New Season"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
@@ -1501,6 +1521,11 @@ msgstr "Nessun risultato trovato."
|
|||||||
msgid "No titles found for this genre."
|
msgid "No titles found for this genre."
|
||||||
msgstr "Nessun titolo trovato per questo genere."
|
msgstr "Nessun titolo trovato per questo genere."
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "No upcoming episodes or releases in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/components/settings/integration-card.tsx
|
#: apps/native/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/integration-card.tsx
|
#: apps/web/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/system-health-section.tsx
|
#: apps/web/src/components/settings/system-health-section.tsx
|
||||||
@@ -1511,6 +1536,7 @@ msgstr "Non configurato"
|
|||||||
msgid "Not Found"
|
msgid "Not Found"
|
||||||
msgstr "Non trovato"
|
msgstr "Non trovato"
|
||||||
|
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
msgid "On Watchlist"
|
msgid "On Watchlist"
|
||||||
msgstr "In watchlist"
|
msgstr "In watchlist"
|
||||||
@@ -1839,6 +1865,7 @@ msgid "Remove from library"
|
|||||||
msgstr "Rimuovi dalla libreria"
|
msgstr "Rimuovi dalla libreria"
|
||||||
|
|
||||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Remove from Library"
|
msgid "Remove from Library"
|
||||||
msgstr "Rimuovi dalla libreria"
|
msgstr "Rimuovi dalla libreria"
|
||||||
@@ -1938,6 +1965,16 @@ msgstr "Rivedi i risultati trovati e scegli cosa importare."
|
|||||||
msgid "Run now"
|
msgid "Run now"
|
||||||
msgstr "Esegui ora"
|
msgstr "Esegui ora"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "S{seasonNum} · {epCount, plural, one {# episode} other {# episodes}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||||
msgid "Saturday"
|
msgid "Saturday"
|
||||||
msgstr "Sabato"
|
msgstr "Sabato"
|
||||||
@@ -2033,6 +2070,11 @@ msgstr "Stagioni"
|
|||||||
msgid "Security"
|
msgid "Security"
|
||||||
msgstr "Sicurezza"
|
msgstr "Sicurezza"
|
||||||
|
|
||||||
|
#: apps/native/src/components/ui/section-header.tsx
|
||||||
|
#: apps/web/src/components/dashboard/feed-section.tsx
|
||||||
|
msgid "See all"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/landing-page.tsx
|
#: apps/web/src/components/landing-page.tsx
|
||||||
msgid "Self-hosted movie & TV tracker"
|
msgid "Self-hosted movie & TV tracker"
|
||||||
msgstr "Tracker film & TV self-hosted"
|
msgstr "Tracker film & TV self-hosted"
|
||||||
@@ -2453,6 +2495,15 @@ msgstr "Non visto S{seasonNum} E{epNum}"
|
|||||||
msgid "Up next"
|
msgid "Up next"
|
||||||
msgstr "Prossimo"
|
msgstr "Prossimo"
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Upcoming"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||||
#~ msgid "Update available: {0}"
|
#~ msgid "Update available: {0}"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -2571,6 +2622,7 @@ msgid "Watched S{seasonNum} E{epNum}"
|
|||||||
msgstr "Visto S{seasonNum} E{epNum}"
|
msgstr "Visto S{seasonNum} E{epNum}"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Watching"
|
msgid "Watching"
|
||||||
@@ -2645,4 +2697,3 @@ msgstr "La tua libreria è vuota"
|
|||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "Your name"
|
msgid "Your name"
|
||||||
msgstr "Il tuo nome"
|
msgstr "Il tuo nome"
|
||||||
|
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ msgid "Catch up"
|
|||||||
msgstr "追いつく"
|
msgstr "追いつく"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Caught Up"
|
msgid "Caught Up"
|
||||||
@@ -514,6 +515,7 @@ msgstr "閉じる"
|
|||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
#: apps/web/src/components/dashboard/stats-display.tsx
|
#: apps/web/src/components/dashboard/stats-display.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Completed"
|
msgid "Completed"
|
||||||
@@ -838,6 +840,10 @@ msgstr "エピソード {period}"
|
|||||||
msgid "Episodes {select}"
|
msgid "Episodes {select}"
|
||||||
msgstr "エピソード {select}"
|
msgstr "エピソード {select}"
|
||||||
|
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Episodes and movies coming up in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#~ msgid "Episodes this week"
|
#~ msgid "Episodes this week"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -1321,6 +1327,7 @@ msgstr "すべて視聴済みにする"
|
|||||||
#~ msgid "Mark as Completed"
|
#~ msgid "Mark as Completed"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Mark as Watched"
|
msgid "Mark as Watched"
|
||||||
msgstr "視聴済みにする"
|
msgstr "視聴済みにする"
|
||||||
@@ -1329,6 +1336,10 @@ msgstr "視聴済みにする"
|
|||||||
#~ msgid "Mark as Watching"
|
#~ msgid "Mark as Watching"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
msgid "Mark Episode Watched"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/title/[id].tsx
|
#: apps/native/src/app/title/[id].tsx
|
||||||
#: apps/web/src/components/titles/title-actions.tsx
|
#: apps/web/src/components/titles/title-actions.tsx
|
||||||
msgid "Mark Watched"
|
msgid "Mark Watched"
|
||||||
@@ -1439,6 +1450,10 @@ msgstr "なし"
|
|||||||
msgid "Never run"
|
msgid "Never run"
|
||||||
msgstr "未実行"
|
msgstr "未実行"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#~ msgid "New"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "New account creation is currently disabled."
|
msgid "New account creation is currently disabled."
|
||||||
msgstr "現在、新規アカウントの作成は無効になっています。"
|
msgstr "現在、新規アカウントの作成は無効になっています。"
|
||||||
@@ -1460,6 +1475,11 @@ msgstr "新しいパスワード"
|
|||||||
msgid "New password must be at least 8 characters"
|
msgid "New password must be at least 8 characters"
|
||||||
msgstr "新しいパスワードは8文字以上である必要があります"
|
msgstr "新しいパスワードは8文字以上である必要があります"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "New Season"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
@@ -1501,6 +1521,11 @@ msgstr "結果が見つかりませんでした。"
|
|||||||
msgid "No titles found for this genre."
|
msgid "No titles found for this genre."
|
||||||
msgstr "このジャンルの作品は見つかりませんでした。"
|
msgstr "このジャンルの作品は見つかりませんでした。"
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "No upcoming episodes or releases in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/components/settings/integration-card.tsx
|
#: apps/native/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/integration-card.tsx
|
#: apps/web/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/system-health-section.tsx
|
#: apps/web/src/components/settings/system-health-section.tsx
|
||||||
@@ -1511,6 +1536,7 @@ msgstr "未設定"
|
|||||||
msgid "Not Found"
|
msgid "Not Found"
|
||||||
msgstr "見つかりません"
|
msgstr "見つかりません"
|
||||||
|
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
msgid "On Watchlist"
|
msgid "On Watchlist"
|
||||||
msgstr "ウォッチリスト内"
|
msgstr "ウォッチリスト内"
|
||||||
@@ -1839,6 +1865,7 @@ msgid "Remove from library"
|
|||||||
msgstr "ライブラリから削除"
|
msgstr "ライブラリから削除"
|
||||||
|
|
||||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Remove from Library"
|
msgid "Remove from Library"
|
||||||
msgstr "ライブラリから削除"
|
msgstr "ライブラリから削除"
|
||||||
@@ -1938,6 +1965,16 @@ msgstr "見つかったものを確認し、インポートする項目を選択
|
|||||||
msgid "Run now"
|
msgid "Run now"
|
||||||
msgstr "今すぐ実行"
|
msgstr "今すぐ実行"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "S{seasonNum} · {epCount, plural, one {# episode} other {# episodes}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||||
msgid "Saturday"
|
msgid "Saturday"
|
||||||
msgstr "土曜日"
|
msgstr "土曜日"
|
||||||
@@ -2033,6 +2070,11 @@ msgstr "シーズン"
|
|||||||
msgid "Security"
|
msgid "Security"
|
||||||
msgstr "セキュリティ"
|
msgstr "セキュリティ"
|
||||||
|
|
||||||
|
#: apps/native/src/components/ui/section-header.tsx
|
||||||
|
#: apps/web/src/components/dashboard/feed-section.tsx
|
||||||
|
msgid "See all"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/landing-page.tsx
|
#: apps/web/src/components/landing-page.tsx
|
||||||
msgid "Self-hosted movie & TV tracker"
|
msgid "Self-hosted movie & TV tracker"
|
||||||
msgstr "セルフホスト型映画・TVトラッカー"
|
msgstr "セルフホスト型映画・TVトラッカー"
|
||||||
@@ -2453,6 +2495,15 @@ msgstr "S{seasonNum} E{epNum}を未視聴にしました"
|
|||||||
msgid "Up next"
|
msgid "Up next"
|
||||||
msgstr "次のエピソード"
|
msgstr "次のエピソード"
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Upcoming"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||||
#~ msgid "Update available: {0}"
|
#~ msgid "Update available: {0}"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -2571,6 +2622,7 @@ msgid "Watched S{seasonNum} E{epNum}"
|
|||||||
msgstr "S{seasonNum} E{epNum}を視聴済みにしました"
|
msgstr "S{seasonNum} E{epNum}を視聴済みにしました"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Watching"
|
msgid "Watching"
|
||||||
@@ -2645,4 +2697,3 @@ msgstr "ライブラリは空です"
|
|||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "Your name"
|
msgid "Your name"
|
||||||
msgstr "お名前"
|
msgstr "お名前"
|
||||||
|
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ msgid "Catch up"
|
|||||||
msgstr "따라잡기"
|
msgstr "따라잡기"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Caught Up"
|
msgid "Caught Up"
|
||||||
@@ -514,6 +515,7 @@ msgstr "닫기"
|
|||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
#: apps/web/src/components/dashboard/stats-display.tsx
|
#: apps/web/src/components/dashboard/stats-display.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Completed"
|
msgid "Completed"
|
||||||
@@ -838,6 +840,10 @@ msgstr "에피소드 {period}"
|
|||||||
msgid "Episodes {select}"
|
msgid "Episodes {select}"
|
||||||
msgstr "에피소드 {select}"
|
msgstr "에피소드 {select}"
|
||||||
|
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Episodes and movies coming up in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#~ msgid "Episodes this week"
|
#~ msgid "Episodes this week"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -1321,6 +1327,7 @@ msgstr "모두 시청 완료로 표시"
|
|||||||
#~ msgid "Mark as Completed"
|
#~ msgid "Mark as Completed"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Mark as Watched"
|
msgid "Mark as Watched"
|
||||||
msgstr "시청 완료로 표시"
|
msgstr "시청 완료로 표시"
|
||||||
@@ -1329,6 +1336,10 @@ msgstr "시청 완료로 표시"
|
|||||||
#~ msgid "Mark as Watching"
|
#~ msgid "Mark as Watching"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
msgid "Mark Episode Watched"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/title/[id].tsx
|
#: apps/native/src/app/title/[id].tsx
|
||||||
#: apps/web/src/components/titles/title-actions.tsx
|
#: apps/web/src/components/titles/title-actions.tsx
|
||||||
msgid "Mark Watched"
|
msgid "Mark Watched"
|
||||||
@@ -1439,6 +1450,10 @@ msgstr "없음"
|
|||||||
msgid "Never run"
|
msgid "Never run"
|
||||||
msgstr "실행된 적 없음"
|
msgstr "실행된 적 없음"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#~ msgid "New"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "New account creation is currently disabled."
|
msgid "New account creation is currently disabled."
|
||||||
msgstr "현재 새 계정 생성이 비활성화되어 있습니다."
|
msgstr "현재 새 계정 생성이 비활성화되어 있습니다."
|
||||||
@@ -1460,6 +1475,11 @@ msgstr "새 비밀번호"
|
|||||||
msgid "New password must be at least 8 characters"
|
msgid "New password must be at least 8 characters"
|
||||||
msgstr "새 비밀번호는 최소 8자 이상이어야 합니다"
|
msgstr "새 비밀번호는 최소 8자 이상이어야 합니다"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "New Season"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
@@ -1501,6 +1521,11 @@ msgstr "결과를 찾을 수 없습니다."
|
|||||||
msgid "No titles found for this genre."
|
msgid "No titles found for this genre."
|
||||||
msgstr "이 장르에 해당하는 작품이 없습니다."
|
msgstr "이 장르에 해당하는 작품이 없습니다."
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "No upcoming episodes or releases in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/components/settings/integration-card.tsx
|
#: apps/native/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/integration-card.tsx
|
#: apps/web/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/system-health-section.tsx
|
#: apps/web/src/components/settings/system-health-section.tsx
|
||||||
@@ -1511,6 +1536,7 @@ msgstr "구성되지 않음"
|
|||||||
msgid "Not Found"
|
msgid "Not Found"
|
||||||
msgstr "찾을 수 없음"
|
msgstr "찾을 수 없음"
|
||||||
|
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
msgid "On Watchlist"
|
msgid "On Watchlist"
|
||||||
msgstr "찜 목록에 있음"
|
msgstr "찜 목록에 있음"
|
||||||
@@ -1839,6 +1865,7 @@ msgid "Remove from library"
|
|||||||
msgstr "라이브러리에서 제거"
|
msgstr "라이브러리에서 제거"
|
||||||
|
|
||||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Remove from Library"
|
msgid "Remove from Library"
|
||||||
msgstr "라이브러리에서 제거"
|
msgstr "라이브러리에서 제거"
|
||||||
@@ -1938,6 +1965,16 @@ msgstr "찾은 항목을 검토하고 가져올 항목을 선택하세요."
|
|||||||
msgid "Run now"
|
msgid "Run now"
|
||||||
msgstr "지금 실행"
|
msgstr "지금 실행"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "S{seasonNum} · {epCount, plural, one {# episode} other {# episodes}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||||
msgid "Saturday"
|
msgid "Saturday"
|
||||||
msgstr "토요일"
|
msgstr "토요일"
|
||||||
@@ -2033,6 +2070,11 @@ msgstr "시즌"
|
|||||||
msgid "Security"
|
msgid "Security"
|
||||||
msgstr "보안"
|
msgstr "보안"
|
||||||
|
|
||||||
|
#: apps/native/src/components/ui/section-header.tsx
|
||||||
|
#: apps/web/src/components/dashboard/feed-section.tsx
|
||||||
|
msgid "See all"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/landing-page.tsx
|
#: apps/web/src/components/landing-page.tsx
|
||||||
msgid "Self-hosted movie & TV tracker"
|
msgid "Self-hosted movie & TV tracker"
|
||||||
msgstr "자체 호스팅 영화 & TV 트래커"
|
msgstr "자체 호스팅 영화 & TV 트래커"
|
||||||
@@ -2453,6 +2495,15 @@ msgstr "S{seasonNum} E{epNum} 시청 취소됨"
|
|||||||
msgid "Up next"
|
msgid "Up next"
|
||||||
msgstr "다음 에피소드"
|
msgstr "다음 에피소드"
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Upcoming"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||||
#~ msgid "Update available: {0}"
|
#~ msgid "Update available: {0}"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -2571,6 +2622,7 @@ msgid "Watched S{seasonNum} E{epNum}"
|
|||||||
msgstr "S{seasonNum} E{epNum} 시청 완료"
|
msgstr "S{seasonNum} E{epNum} 시청 완료"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Watching"
|
msgid "Watching"
|
||||||
@@ -2645,4 +2697,3 @@ msgstr "라이브러리가 비어 있습니다"
|
|||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "Your name"
|
msgid "Your name"
|
||||||
msgstr "이름"
|
msgstr "이름"
|
||||||
|
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ msgid "Catch up"
|
|||||||
msgstr "Inhalen"
|
msgstr "Inhalen"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Caught Up"
|
msgid "Caught Up"
|
||||||
@@ -514,6 +515,7 @@ msgstr "Sluiten"
|
|||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
#: apps/web/src/components/dashboard/stats-display.tsx
|
#: apps/web/src/components/dashboard/stats-display.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Completed"
|
msgid "Completed"
|
||||||
@@ -838,6 +840,10 @@ msgstr "Afleveringen {period}"
|
|||||||
msgid "Episodes {select}"
|
msgid "Episodes {select}"
|
||||||
msgstr "Afleveringen {select}"
|
msgstr "Afleveringen {select}"
|
||||||
|
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Episodes and movies coming up in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#~ msgid "Episodes this week"
|
#~ msgid "Episodes this week"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -1321,6 +1327,7 @@ msgstr "Alles als bekeken markeren"
|
|||||||
#~ msgid "Mark as Completed"
|
#~ msgid "Mark as Completed"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Mark as Watched"
|
msgid "Mark as Watched"
|
||||||
msgstr "Markeren als bekeken"
|
msgstr "Markeren als bekeken"
|
||||||
@@ -1329,6 +1336,10 @@ msgstr "Markeren als bekeken"
|
|||||||
#~ msgid "Mark as Watching"
|
#~ msgid "Mark as Watching"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
msgid "Mark Episode Watched"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/title/[id].tsx
|
#: apps/native/src/app/title/[id].tsx
|
||||||
#: apps/web/src/components/titles/title-actions.tsx
|
#: apps/web/src/components/titles/title-actions.tsx
|
||||||
msgid "Mark Watched"
|
msgid "Mark Watched"
|
||||||
@@ -1439,6 +1450,10 @@ msgstr "Nooit"
|
|||||||
msgid "Never run"
|
msgid "Never run"
|
||||||
msgstr "Nooit uitgevoerd"
|
msgstr "Nooit uitgevoerd"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#~ msgid "New"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "New account creation is currently disabled."
|
msgid "New account creation is currently disabled."
|
||||||
msgstr "Aanmaken van nieuwe accounts is momenteel uitgeschakeld."
|
msgstr "Aanmaken van nieuwe accounts is momenteel uitgeschakeld."
|
||||||
@@ -1460,6 +1475,11 @@ msgstr "Nieuw wachtwoord"
|
|||||||
msgid "New password must be at least 8 characters"
|
msgid "New password must be at least 8 characters"
|
||||||
msgstr "Nieuw wachtwoord moet minimaal 8 tekens bevatten"
|
msgstr "Nieuw wachtwoord moet minimaal 8 tekens bevatten"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "New Season"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
@@ -1501,6 +1521,11 @@ msgstr "Geen resultaten gevonden."
|
|||||||
msgid "No titles found for this genre."
|
msgid "No titles found for this genre."
|
||||||
msgstr "Geen titels gevonden voor dit genre."
|
msgstr "Geen titels gevonden voor dit genre."
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "No upcoming episodes or releases in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/components/settings/integration-card.tsx
|
#: apps/native/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/integration-card.tsx
|
#: apps/web/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/system-health-section.tsx
|
#: apps/web/src/components/settings/system-health-section.tsx
|
||||||
@@ -1511,6 +1536,7 @@ msgstr "Niet geconfigureerd"
|
|||||||
msgid "Not Found"
|
msgid "Not Found"
|
||||||
msgstr "Niet gevonden"
|
msgstr "Niet gevonden"
|
||||||
|
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
msgid "On Watchlist"
|
msgid "On Watchlist"
|
||||||
msgstr "Op kijklijst"
|
msgstr "Op kijklijst"
|
||||||
@@ -1839,6 +1865,7 @@ msgid "Remove from library"
|
|||||||
msgstr "Verwijderen uit bibliotheek"
|
msgstr "Verwijderen uit bibliotheek"
|
||||||
|
|
||||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Remove from Library"
|
msgid "Remove from Library"
|
||||||
msgstr "Verwijderen uit bibliotheek"
|
msgstr "Verwijderen uit bibliotheek"
|
||||||
@@ -1938,6 +1965,16 @@ msgstr "Bekijk wat er gevonden is en kies wat je wilt importeren."
|
|||||||
msgid "Run now"
|
msgid "Run now"
|
||||||
msgstr "Nu uitvoeren"
|
msgstr "Nu uitvoeren"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "S{seasonNum} · {epCount, plural, one {# episode} other {# episodes}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||||
msgid "Saturday"
|
msgid "Saturday"
|
||||||
msgstr "Zaterdag"
|
msgstr "Zaterdag"
|
||||||
@@ -2033,6 +2070,11 @@ msgstr "Seizoenen"
|
|||||||
msgid "Security"
|
msgid "Security"
|
||||||
msgstr "Beveiliging"
|
msgstr "Beveiliging"
|
||||||
|
|
||||||
|
#: apps/native/src/components/ui/section-header.tsx
|
||||||
|
#: apps/web/src/components/dashboard/feed-section.tsx
|
||||||
|
msgid "See all"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/landing-page.tsx
|
#: apps/web/src/components/landing-page.tsx
|
||||||
msgid "Self-hosted movie & TV tracker"
|
msgid "Self-hosted movie & TV tracker"
|
||||||
msgstr "Zelfgehoste film- & tv-tracker"
|
msgstr "Zelfgehoste film- & tv-tracker"
|
||||||
@@ -2453,6 +2495,15 @@ msgstr "Niet bekeken S{seasonNum} E{epNum}"
|
|||||||
msgid "Up next"
|
msgid "Up next"
|
||||||
msgstr "Volgende"
|
msgstr "Volgende"
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Upcoming"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||||
#~ msgid "Update available: {0}"
|
#~ msgid "Update available: {0}"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -2571,6 +2622,7 @@ msgid "Watched S{seasonNum} E{epNum}"
|
|||||||
msgstr "Bekeken S{seasonNum} E{epNum}"
|
msgstr "Bekeken S{seasonNum} E{epNum}"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Watching"
|
msgid "Watching"
|
||||||
@@ -2645,4 +2697,3 @@ msgstr "Je bibliotheek is leeg"
|
|||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "Your name"
|
msgid "Your name"
|
||||||
msgstr "Jouw naam"
|
msgstr "Jouw naam"
|
||||||
|
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ msgid "Catch up"
|
|||||||
msgstr "Colocar em dia"
|
msgstr "Colocar em dia"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Caught Up"
|
msgid "Caught Up"
|
||||||
@@ -514,6 +515,7 @@ msgstr "Fechar"
|
|||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
#: apps/web/src/components/dashboard/stats-display.tsx
|
#: apps/web/src/components/dashboard/stats-display.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Completed"
|
msgid "Completed"
|
||||||
@@ -838,6 +840,10 @@ msgstr ""
|
|||||||
msgid "Episodes {select}"
|
msgid "Episodes {select}"
|
||||||
msgstr "Episódios {select}"
|
msgstr "Episódios {select}"
|
||||||
|
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Episodes and movies coming up in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#~ msgid "Episodes this week"
|
#~ msgid "Episodes this week"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -1321,6 +1327,7 @@ msgstr "Marcar Todos como Assistidos"
|
|||||||
#~ msgid "Mark as Completed"
|
#~ msgid "Mark as Completed"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Mark as Watched"
|
msgid "Mark as Watched"
|
||||||
msgstr "Marcar como Assistido"
|
msgstr "Marcar como Assistido"
|
||||||
@@ -1329,6 +1336,10 @@ msgstr "Marcar como Assistido"
|
|||||||
#~ msgid "Mark as Watching"
|
#~ msgid "Mark as Watching"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
msgid "Mark Episode Watched"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/title/[id].tsx
|
#: apps/native/src/app/title/[id].tsx
|
||||||
#: apps/web/src/components/titles/title-actions.tsx
|
#: apps/web/src/components/titles/title-actions.tsx
|
||||||
msgid "Mark Watched"
|
msgid "Mark Watched"
|
||||||
@@ -1439,6 +1450,10 @@ msgstr "Nunca"
|
|||||||
msgid "Never run"
|
msgid "Never run"
|
||||||
msgstr "Nunca executado"
|
msgstr "Nunca executado"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#~ msgid "New"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "New account creation is currently disabled."
|
msgid "New account creation is currently disabled."
|
||||||
msgstr "A criação de novas contas está desativada no momento."
|
msgstr "A criação de novas contas está desativada no momento."
|
||||||
@@ -1460,6 +1475,11 @@ msgstr "Nova senha"
|
|||||||
msgid "New password must be at least 8 characters"
|
msgid "New password must be at least 8 characters"
|
||||||
msgstr "A nova senha deve ter pelo menos 8 caracteres"
|
msgstr "A nova senha deve ter pelo menos 8 caracteres"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "New Season"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
@@ -1501,6 +1521,11 @@ msgstr "Nenhum resultado encontrado."
|
|||||||
msgid "No titles found for this genre."
|
msgid "No titles found for this genre."
|
||||||
msgstr "Nenhum título encontrado para este gênero."
|
msgstr "Nenhum título encontrado para este gênero."
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "No upcoming episodes or releases in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/components/settings/integration-card.tsx
|
#: apps/native/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/integration-card.tsx
|
#: apps/web/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/system-health-section.tsx
|
#: apps/web/src/components/settings/system-health-section.tsx
|
||||||
@@ -1511,6 +1536,7 @@ msgstr "Não configurado"
|
|||||||
msgid "Not Found"
|
msgid "Not Found"
|
||||||
msgstr "Não Encontrado"
|
msgstr "Não Encontrado"
|
||||||
|
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
msgid "On Watchlist"
|
msgid "On Watchlist"
|
||||||
msgstr "Na Lista de Desejos"
|
msgstr "Na Lista de Desejos"
|
||||||
@@ -1839,6 +1865,7 @@ msgid "Remove from library"
|
|||||||
msgstr "Remover da biblioteca"
|
msgstr "Remover da biblioteca"
|
||||||
|
|
||||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Remove from Library"
|
msgid "Remove from Library"
|
||||||
msgstr "Remover da Biblioteca"
|
msgstr "Remover da Biblioteca"
|
||||||
@@ -1938,6 +1965,16 @@ msgstr "Revise o que foi encontrado e escolha o que importar."
|
|||||||
msgid "Run now"
|
msgid "Run now"
|
||||||
msgstr "Executar agora"
|
msgstr "Executar agora"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "S{seasonNum} · {epCount, plural, one {# episode} other {# episodes}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||||
msgid "Saturday"
|
msgid "Saturday"
|
||||||
msgstr "Sábado"
|
msgstr "Sábado"
|
||||||
@@ -2033,6 +2070,11 @@ msgstr "Temporadas"
|
|||||||
msgid "Security"
|
msgid "Security"
|
||||||
msgstr "Segurança"
|
msgstr "Segurança"
|
||||||
|
|
||||||
|
#: apps/native/src/components/ui/section-header.tsx
|
||||||
|
#: apps/web/src/components/dashboard/feed-section.tsx
|
||||||
|
msgid "See all"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/landing-page.tsx
|
#: apps/web/src/components/landing-page.tsx
|
||||||
msgid "Self-hosted movie & TV tracker"
|
msgid "Self-hosted movie & TV tracker"
|
||||||
msgstr "Rastreador de filmes e séries auto-hospedado"
|
msgstr "Rastreador de filmes e séries auto-hospedado"
|
||||||
@@ -2453,6 +2495,15 @@ msgstr "Desmarcado T{seasonNum} E{epNum}"
|
|||||||
msgid "Up next"
|
msgid "Up next"
|
||||||
msgstr "A seguir"
|
msgstr "A seguir"
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Upcoming"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||||
#~ msgid "Update available: {0}"
|
#~ msgid "Update available: {0}"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -2571,6 +2622,7 @@ msgid "Watched S{seasonNum} E{epNum}"
|
|||||||
msgstr "Assistido T{seasonNum} E{epNum}"
|
msgstr "Assistido T{seasonNum} E{epNum}"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Watching"
|
msgid "Watching"
|
||||||
@@ -2645,4 +2697,3 @@ msgstr "Sua biblioteca está vazia"
|
|||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "Your name"
|
msgid "Your name"
|
||||||
msgstr "Seu nome"
|
msgstr "Seu nome"
|
||||||
|
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ msgid "Catch up"
|
|||||||
msgstr "追上"
|
msgstr "追上"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Caught Up"
|
msgid "Caught Up"
|
||||||
@@ -514,6 +515,7 @@ msgstr "关闭"
|
|||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
#: apps/web/src/components/dashboard/stats-display.tsx
|
#: apps/web/src/components/dashboard/stats-display.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Completed"
|
msgid "Completed"
|
||||||
@@ -838,6 +840,10 @@ msgstr "剧集 {period}"
|
|||||||
msgid "Episodes {select}"
|
msgid "Episodes {select}"
|
||||||
msgstr "剧集 {select}"
|
msgstr "剧集 {select}"
|
||||||
|
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Episodes and movies coming up in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
#: apps/native/src/app/(tabs)/(home)/index.tsx
|
||||||
#~ msgid "Episodes this week"
|
#~ msgid "Episodes this week"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -1321,6 +1327,7 @@ msgstr "全部标记为已观看"
|
|||||||
#~ msgid "Mark as Completed"
|
#~ msgid "Mark as Completed"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Mark as Watched"
|
msgid "Mark as Watched"
|
||||||
msgstr "标记为已观看"
|
msgstr "标记为已观看"
|
||||||
@@ -1329,6 +1336,10 @@ msgstr "标记为已观看"
|
|||||||
#~ msgid "Mark as Watching"
|
#~ msgid "Mark as Watching"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
msgid "Mark Episode Watched"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/title/[id].tsx
|
#: apps/native/src/app/title/[id].tsx
|
||||||
#: apps/web/src/components/titles/title-actions.tsx
|
#: apps/web/src/components/titles/title-actions.tsx
|
||||||
msgid "Mark Watched"
|
msgid "Mark Watched"
|
||||||
@@ -1439,6 +1450,10 @@ msgstr "从未"
|
|||||||
msgid "Never run"
|
msgid "Never run"
|
||||||
msgstr "从未运行"
|
msgstr "从未运行"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#~ msgid "New"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "New account creation is currently disabled."
|
msgid "New account creation is currently disabled."
|
||||||
msgstr "当前已禁用新账户创建。"
|
msgstr "当前已禁用新账户创建。"
|
||||||
@@ -1460,6 +1475,11 @@ msgstr "新密码"
|
|||||||
msgid "New password must be at least 8 characters"
|
msgid "New password must be at least 8 characters"
|
||||||
msgstr "新密码至少需要 8 个字符"
|
msgstr "新密码至少需要 8 个字符"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "New Season"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
#: apps/web/src/components/people/filmography-grid.tsx
|
#: apps/web/src/components/people/filmography-grid.tsx
|
||||||
msgid "Newest"
|
msgid "Newest"
|
||||||
@@ -1501,6 +1521,11 @@ msgstr "未找到结果。"
|
|||||||
msgid "No titles found for this genre."
|
msgid "No titles found for this genre."
|
||||||
msgstr "此类型下未找到任何内容。"
|
msgstr "此类型下未找到任何内容。"
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "No upcoming episodes or releases in the next 90 days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/components/settings/integration-card.tsx
|
#: apps/native/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/integration-card.tsx
|
#: apps/web/src/components/settings/integration-card.tsx
|
||||||
#: apps/web/src/components/settings/system-health-section.tsx
|
#: apps/web/src/components/settings/system-health-section.tsx
|
||||||
@@ -1511,6 +1536,7 @@ msgstr "未配置"
|
|||||||
msgid "Not Found"
|
msgid "Not Found"
|
||||||
msgstr "未找到"
|
msgstr "未找到"
|
||||||
|
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
msgid "On Watchlist"
|
msgid "On Watchlist"
|
||||||
msgstr "在待看列表中"
|
msgstr "在待看列表中"
|
||||||
@@ -1839,6 +1865,7 @@ msgid "Remove from library"
|
|||||||
msgstr "从媒体库中移除"
|
msgstr "从媒体库中移除"
|
||||||
|
|
||||||
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
#: apps/native/src/components/dashboard/continue-watching-card.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
#: apps/native/src/components/ui/poster-card.tsx
|
#: apps/native/src/components/ui/poster-card.tsx
|
||||||
msgid "Remove from Library"
|
msgid "Remove from Library"
|
||||||
msgstr "从媒体库中移除"
|
msgstr "从媒体库中移除"
|
||||||
@@ -1938,6 +1965,16 @@ msgstr "查看已发现的内容,选择要导入的项目。"
|
|||||||
msgid "Run now"
|
msgid "Run now"
|
||||||
msgstr "立即运行"
|
msgstr "立即运行"
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
#~ msgid "S{0} · {1} {2, plural, one {episode} other {episodes}}"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-row.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
|
msgid "S{seasonNum} · {epCount, plural, one {# episode} other {# episodes}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
#: apps/web/src/components/settings/backup-schedule-section.tsx
|
||||||
msgid "Saturday"
|
msgid "Saturday"
|
||||||
msgstr "星期六"
|
msgstr "星期六"
|
||||||
@@ -2033,6 +2070,11 @@ msgstr "季"
|
|||||||
msgid "Security"
|
msgid "Security"
|
||||||
msgstr "安全"
|
msgstr "安全"
|
||||||
|
|
||||||
|
#: apps/native/src/components/ui/section-header.tsx
|
||||||
|
#: apps/web/src/components/dashboard/feed-section.tsx
|
||||||
|
msgid "See all"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/web/src/components/landing-page.tsx
|
#: apps/web/src/components/landing-page.tsx
|
||||||
msgid "Self-hosted movie & TV tracker"
|
msgid "Self-hosted movie & TV tracker"
|
||||||
msgstr "自托管电影和电视剧追踪器"
|
msgstr "自托管电影和电视剧追踪器"
|
||||||
@@ -2453,6 +2495,15 @@ msgstr "第 {seasonNum} 季第 {epNum} 集已取消标记"
|
|||||||
msgid "Up next"
|
msgid "Up next"
|
||||||
msgstr "接下来"
|
msgstr "接下来"
|
||||||
|
|
||||||
|
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
|
||||||
|
#: apps/native/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-section.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/components/nav-bar.tsx
|
||||||
|
#: apps/web/src/routes/_app/upcoming.tsx
|
||||||
|
msgid "Upcoming"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
#: apps/native/src/app/(tabs)/(settings)/index.tsx
|
||||||
#~ msgid "Update available: {0}"
|
#~ msgid "Update available: {0}"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
@@ -2571,6 +2622,7 @@ msgid "Watched S{seasonNum} E{epNum}"
|
|||||||
msgstr "第 {seasonNum} 季第 {epNum} 集已标记为已看"
|
msgstr "第 {seasonNum} 季第 {epNum} 集已标记为已看"
|
||||||
|
|
||||||
#: apps/native/src/components/titles/status-action-button.tsx
|
#: apps/native/src/components/titles/status-action-button.tsx
|
||||||
|
#: apps/web/src/components/dashboard/upcoming-item.tsx
|
||||||
#: apps/web/src/components/title-card.tsx
|
#: apps/web/src/components/title-card.tsx
|
||||||
#: apps/web/src/components/titles/status-button.tsx
|
#: apps/web/src/components/titles/status-button.tsx
|
||||||
msgid "Watching"
|
msgid "Watching"
|
||||||
@@ -2645,4 +2697,3 @@ msgstr "您的媒体库为空"
|
|||||||
#: apps/native/src/app/(auth)/register.tsx
|
#: apps/native/src/app/(auth)/register.tsx
|
||||||
msgid "Your name"
|
msgid "Your name"
|
||||||
msgstr "您的姓名"
|
msgstr "您的姓名"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user