feat: add upcoming episodes and movies timeline (#17)

This commit is contained in:
2026-03-21 12:31:53 -04:00
committed by GitHub
parent 5aaf88591b
commit 1a503df2ae
42 changed files with 2405 additions and 61 deletions
+8 -2
View File
@@ -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);
+1
View File
@@ -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>
);
}
+12 -2
View File
@@ -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={{
+21
View File
@@ -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,
} }
+15 -2
View File
@@ -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>
+10 -2
View File
@@ -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}
+1 -1
View File
@@ -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" />
+2 -2
View File
@@ -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" />
+122
View File
@@ -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
View File
@@ -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",
+14
View File
@@ -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",
+58
View File
@@ -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>;
+221
View File
@@ -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 [];
+293
View File
@@ -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();
});
});
+74 -1
View File
@@ -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();
}
+1
View File
@@ -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),
], ],
); );
+23 -2
View File
@@ -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 };
+1
View File
@@ -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": {
+78
View File
@@ -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 };
});
}
+52 -1
View File
@@ -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 "اسمك"
+52 -1
View File
@@ -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"
+52
View File
@@ -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"
+52 -1
View File
@@ -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"
+52 -1
View File
@@ -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"
+52 -1
View File
@@ -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 "שמך"
+52 -1
View File
@@ -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"
+52 -1
View File
@@ -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 "お名前"
+52 -1
View File
@@ -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 "이름"
+52 -1
View File
@@ -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"
+52 -1
View File
@@ -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"
+52 -1
View File
@@ -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 "您的姓名"