feat(upcoming): add a "Recently aired" view of unwatched episodes

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Sonnet 5.5 committed 2026-10-05 14:29:47 -04:00
1 parent e3f7ccca07
commit e625f319a2
24 files changed
+671 -78

No files matched your search

+48 -26
View File
@@ -54,6 +54,8 @@ export default function UpcomingScreen() {
const backgroundColor = useCSSVariable("--color-background") as string;
const mutedColor = useCSSVariable("--color-muted-foreground") as string;
const [view, setView] = useState<"upcoming" | "recent">("upcoming");
const isRecent = view === "recent";
const [mediaType, setMediaType] = useState<"all" | "movie" | "tv">("all");
const [statusFilter, setStatusFilter] = useState<"all" | "watching" | "watchlist">("all");
@@ -64,8 +66,9 @@ export default function UpcomingScreen() {
days: 90,
limit: 20,
cursor: pageParam,
mediaType: mediaType !== "all" ? mediaType : undefined,
statusFilter: statusFilter !== "all" ? [statusFilter] : undefined,
direction: view,
mediaType: !isRecent && mediaType !== "all" ? mediaType : undefined,
statusFilter: !isRecent && statusFilter !== "all" ? [statusFilter] : undefined,
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
@@ -75,12 +78,12 @@ export default function UpcomingScreen() {
const allItems = useMemo(() => data?.pages.flatMap((p) => p.items) ?? [], [data]);
const sections = useMemo(
() =>
groupByDateBucket(allItems).map((b) => ({
groupByDateBucket(allItems, { past: view === "recent" }).map((b) => ({
key: b.key,
title: b.label,
data: b.items,
})),
[allItems],
[allItems, view],
);
const refreshUpcoming = useCallback(
@@ -102,31 +105,46 @@ export default function UpcomingScreen() {
contentContainerStyle={{ paddingHorizontal: 16, gap: 6, paddingTop: 10, paddingBottom: 6 }}
>
<FilterChip
label={t`All`}
isSelected={mediaType === "all"}
onPress={() => setMediaType("all")}
label={t`Upcoming`}
isSelected={view === "upcoming"}
onPress={() => setView("upcoming")}
/>
<FilterChip
label={t`Movies`}
isSelected={mediaType === "movie"}
onPress={() => setMediaType("movie")}
/>
<FilterChip
label={t`TV`}
isSelected={mediaType === "tv"}
onPress={() => setMediaType("tv")}
label={t`Recently aired`}
isSelected={view === "recent"}
onPress={() => setView("recent")}
/>
<View className="bg-border/30 mx-1 w-px self-stretch" />
<FilterChip
label={t`Watching`}
isSelected={statusFilter === "watching"}
onPress={() => setStatusFilter(statusFilter === "watching" ? "all" : "watching")}
/>
<FilterChip
label={t`Watchlist`}
isSelected={statusFilter === "watchlist"}
onPress={() => setStatusFilter(statusFilter === "watchlist" ? "all" : "watchlist")}
/>
{!isRecent && (
<>
<FilterChip
label={t`All`}
isSelected={mediaType === "all"}
onPress={() => setMediaType("all")}
/>
<FilterChip
label={t`Movies`}
isSelected={mediaType === "movie"}
onPress={() => setMediaType("movie")}
/>
<FilterChip
label={t`TV`}
isSelected={mediaType === "tv"}
onPress={() => setMediaType("tv")}
/>
<View className="bg-border/30 mx-1 w-px self-stretch" />
<FilterChip
label={t`Watching`}
isSelected={statusFilter === "watching"}
onPress={() => setStatusFilter(statusFilter === "watching" ? "all" : "watching")}
/>
<FilterChip
label={t`Watchlist`}
isSelected={statusFilter === "watchlist"}
onPress={() => setStatusFilter(statusFilter === "watchlist" ? "all" : "watchlist")}
/>
</>
)}
</ScrollView>
);
@@ -185,7 +203,11 @@ export default function UpcomingScreen() {
>
<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>
{isRecent ? (
<Trans>You're all caught up on the last 90 days.</Trans>
) : (
<Trans>No upcoming episodes or releases in the next 90 days.</Trans>
)}
</Text>
</Animated.View>
)
@@ -84,6 +84,7 @@ export const upcoming = os.library.upcoming.use(authed).handler(({ input, contex
cursor: input.cursor,
mediaType: input.mediaType,
statusFilter: input.statusFilter,
direction: input.direction,
});
return {
items: result.items.map((item) => ({
+71 -29
View File
@@ -14,6 +14,7 @@ import { orpc } from "@/lib/orpc/client";
import { groupByDateBucket } from "@sofa/i18n/date-buckets";
const upcomingSearchSchema = z.object({
view: z.enum(["upcoming", "recent"]).optional().catch(undefined),
type: z.enum(["all", "movie", "tv"]).optional().catch(undefined),
status: z.enum(["all", "watching", "watchlist"]).optional().catch(undefined),
});
@@ -65,6 +66,8 @@ function UpcomingPage() {
const search = Route.useSearch();
const navigate = useNavigate({ from: Route.fullPath });
const view = search.view ?? "upcoming";
const isRecent = view === "recent";
const typeFilter = search.type ?? "all";
const statusFilter = search.status ?? "all";
@@ -75,9 +78,12 @@ function UpcomingPage() {
days: 90,
limit: 20,
cursor: pageParam,
mediaType: typeFilter !== "all" ? (typeFilter as "movie" | "tv") : undefined,
direction: view,
mediaType: !isRecent && typeFilter !== "all" ? (typeFilter as "movie" | "tv") : undefined,
statusFilter:
statusFilter !== "all" ? [statusFilter as "watching" | "watchlist"] : undefined,
!isRecent && statusFilter !== "all"
? [statusFilter as "watching" | "watchlist"]
: undefined,
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
@@ -91,6 +97,16 @@ function UpcomingPage() {
isFetchNextPageError,
});
function setView(value: "upcoming" | "recent") {
void navigate({
search: (prev) => ({
...prev,
view: value === "upcoming" ? undefined : value,
}),
replace: true,
});
}
function setTypeFilter(value: string) {
void navigate({
search: (prev) => ({
@@ -112,33 +128,49 @@ function UpcomingPage() {
}
const filterToggles = (
<div className="flex flex-wrap gap-3">
<div className="space-y-3">
<ToggleGroup
value={[typeFilter]}
value={[view]}
onValueChange={(values) => {
const next = values.find((v) => v !== typeFilter) ?? "all";
setTypeFilter(next);
const next = values.find((v) => v !== view);
if (next === "upcoming" || next === "recent") setView(next);
}}
variant="outline"
size="sm"
>
<ToggleGroupItem value="all">{t`All`}</ToggleGroupItem>
<ToggleGroupItem value="movie">{t`Movies`}</ToggleGroupItem>
<ToggleGroupItem value="tv">{t`TV Shows`}</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup
value={[statusFilter]}
onValueChange={(values) => {
const next = values.find((v) => v !== statusFilter) ?? "all";
setStatusFilter(next);
}}
variant="outline"
size="sm"
>
<ToggleGroupItem value="all">{t`All`}</ToggleGroupItem>
<ToggleGroupItem value="watching">{t`Watching`}</ToggleGroupItem>
<ToggleGroupItem value="watchlist">{t`Watchlist`}</ToggleGroupItem>
<ToggleGroupItem value="upcoming">{t`Upcoming`}</ToggleGroupItem>
<ToggleGroupItem value="recent">{t`Recently aired`}</ToggleGroupItem>
</ToggleGroup>
{!isRecent && (
<div className="flex flex-wrap gap-3">
<ToggleGroup
value={[typeFilter]}
onValueChange={(values) => {
const next = values.find((v) => v !== typeFilter) ?? "all";
setTypeFilter(next);
}}
variant="outline"
size="sm"
>
<ToggleGroupItem value="all">{t`All`}</ToggleGroupItem>
<ToggleGroupItem value="movie">{t`Movies`}</ToggleGroupItem>
<ToggleGroupItem value="tv">{t`TV Shows`}</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup
value={[statusFilter]}
onValueChange={(values) => {
const next = values.find((v) => v !== statusFilter) ?? "all";
setStatusFilter(next);
}}
variant="outline"
size="sm"
>
<ToggleGroupItem value="all">{t`All`}</ToggleGroupItem>
<ToggleGroupItem value="watching">{t`Watching`}</ToggleGroupItem>
<ToggleGroupItem value="watchlist">{t`Watchlist`}</ToggleGroupItem>
</ToggleGroup>
</div>
)}
</div>
);
@@ -149,23 +181,27 @@ function UpcomingPage() {
if (allItems.length === 0) {
return (
<div className="mx-auto max-w-2xl space-y-6">
<UpcomingHeader />
<UpcomingHeader view={view} />
{filterToggles}
<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>
{isRecent ? (
<Trans>You're all caught up on the last 90 days.</Trans>
) : (
<Trans>No upcoming episodes or releases in the next 90 days.</Trans>
)}
</p>
</div>
</div>
);
}
const buckets = groupByDateBucket(allItems);
const buckets = groupByDateBucket(allItems, { past: isRecent });
return (
<div className="mx-auto max-w-2xl space-y-6">
<UpcomingHeader />
<UpcomingHeader view={view} />
{filterToggles}
{buckets.map((bucket) => (
<section key={bucket.key}>
@@ -185,13 +221,19 @@ function UpcomingPage() {
);
}
function UpcomingHeader() {
function UpcomingHeader({ view }: { view: "upcoming" | "recent" }) {
const { t } = useLingui();
return (
<div>
<h1 className="font-display text-2xl tracking-tight">{t`Upcoming`}</h1>
<h1 className="font-display text-2xl tracking-tight">
{view === "recent" ? t`Recently aired` : t`Upcoming`}
</h1>
<p className="text-muted-foreground mt-1 text-sm">
<Trans>Episodes and movies coming up in the next 90 days.</Trans>
{view === "recent" ? (
<Trans>Unwatched episodes from the last 90 days.</Trans>
) : (
<Trans>Episodes and movies coming up in the next 90 days.</Trans>
)}
</p>
</div>
);
@@ -10,8 +10,10 @@ _openapi:
structuredData:
headings: []
contents:
- content: Fetch upcoming episodes and movie releases for titles in the user's
library, sorted by date. Supports cursor-based pagination.
- content: 'Fetch upcoming episodes and movie releases for titles in the user''s
library, sorted by date. Supports cursor-based pagination. Pass
direction: "recent" for unwatched episodes that aired in the past
days, newest first.'
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
+17 -2
View File
@@ -2916,7 +2916,7 @@
"get": {
"operationId": "library.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.",
"description": "Fetch upcoming episodes and movie releases for titles in the user's library, sorted by date. Supports cursor-based pagination. Pass direction: \"recent\" for unwatched episodes that aired in the past days, newest first.",
"tags": [
"Library"
],
@@ -2929,7 +2929,22 @@
"minimum": 1,
"maximum": 90,
"default": 90,
"description": "How many days into the future to look"
"description": "How many days ahead (upcoming) or back (recent) to look"
},
"allowEmptyValue": true,
"allowReserved": true
},
{
"name": "direction",
"in": "query",
"schema": {
"enum": [
"upcoming",
"recent"
],
"type": "string",
"default": "upcoming",
"description": "\"upcoming\": the next `days` days. \"recent\": unwatched TV episodes that aired in the past `days` days (excluding today), newest first"
},
"allowEmptyValue": true,
"allowReserved": true
+1 -1
View File
@@ -238,7 +238,7 @@ export const contract = {
tags: ["Library"],
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.",
'Fetch upcoming episodes and movie releases for titles in the user\'s library, sorted by date. Supports cursor-based pagination. Pass direction: "recent" for unwatched episodes that aired in the past days, newest first.',
successDescription: "Upcoming items sorted by date with streaming info",
})
.input(UpcomingInput)
+7 -1
View File
@@ -663,7 +663,13 @@ export const UpcomingInput = z
.min(1)
.max(90)
.default(90)
.describe("How many days into the future to look"),
.describe("How many days ahead (upcoming) or back (recent) to look"),
direction: z
.enum(["upcoming", "recent"])
.default("upcoming")
.describe(
'"upcoming": the next `days` days. "recent": unwatched TV episodes that aired in the past `days` days (excluding today), newest first',
),
limit: z.number().int().min(1).max(50).default(20).describe("Maximum items per page"),
cursor: z.string().optional().describe("Pagination cursor"),
mediaType: z
+51 -14
View File
@@ -17,6 +17,7 @@ import {
getTitleByIdOrNull,
getTitlesByIds,
getTvTitlesByIds,
getRecentUnwatchedEpisodes,
getUpcomingEpisodes,
getUpcomingMovies,
getUserStatusCounts,
@@ -401,6 +402,17 @@ function compareUpcomingKeys(a: UpcomingCursorKey, b: UpcomingCursorKey): number
);
}
/** Same as compareUpcomingKeys, but dates descend (newest first). */
function compareRecentKeys(a: UpcomingCursorKey, b: UpcomingCursorKey): number {
return (
b.d.localeCompare(a.d) ||
a.n.localeCompare(b.n) ||
a.i.localeCompare(b.i) ||
a.s - b.s ||
a.e - b.e
);
}
function encodeUpcomingCursor(key: UpcomingCursorKey): string {
return Buffer.from(JSON.stringify(key), "utf8").toString("base64url");
}
@@ -435,27 +447,52 @@ export function getUpcomingFeed(
cursor?: string;
mediaType?: "movie" | "tv";
statusFilter?: ("watching" | "watchlist")[];
direction?: "upcoming" | "recent";
} = {},
): UpcomingFeedResult {
const { days = 90, limit = 20, cursor, mediaType, statusFilter } = options;
const {
days = 90,
limit = 20,
cursor,
mediaType,
statusFilter,
direction = "upcoming",
} = options;
const isRecent = direction === "recent";
const compareKeys = isRecent ? compareRecentKeys : compareUpcomingKeys;
// Map display status filter to stored statuses for DB query
const storedStatuses = statusFilter?.map((s) => (s === "watching" ? "in_progress" : s));
const now = new Date();
const today = localDateString(now);
const horizon = new Date();
horizon.setDate(horizon.getDate() + days);
const toDate = localDateString(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.
const cursorKey = cursor ? decodeUpcomingCursor(cursor) : null;
const fromDate = cursorKey?.d ?? today;
const episodeRows =
mediaType === "movie" ? [] : getUpcomingEpisodes(userId, fromDate, toDate, storedStatuses);
const movieRows =
mediaType === "tv" ? [] : getUpcomingMovies(userId, fromDate, toDate, storedStatuses);
let episodeRows: ReturnType<typeof getUpcomingEpisodes>;
let movieRows: ReturnType<typeof getUpcomingMovies>;
if (isRecent) {
// Dates go down: the cursor date is the upper bound, `days` ago the lower bound.
const yesterdayDate = new Date(now);
yesterdayDate.setDate(yesterdayDate.getDate() - 1);
const earliest = new Date(now);
earliest.setDate(earliest.getDate() - days);
const toDate = cursorKey?.d ?? localDateString(yesterdayDate);
const fromDate = localDateString(earliest);
episodeRows = mediaType === "movie" ? [] : getRecentUnwatchedEpisodes(userId, fromDate, toDate);
movieRows = [];
} else {
const horizon = new Date();
horizon.setDate(horizon.getDate() + days);
const toDate = localDateString(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.
const fromDate = cursorKey?.d ?? today;
episodeRows =
mediaType === "movie" ? [] : getUpcomingEpisodes(userId, fromDate, toDate, storedStatuses);
movieRows =
mediaType === "tv" ? [] : getUpcomingMovies(userId, fromDate, toDate, storedStatuses);
}
// Merge into unified items
type RawItem = {
@@ -494,7 +531,7 @@ export function getUpcomingFeed(
];
// Sort by (date, title, titleId, season, episode) ASC — the same key the cursor uses
merged.sort((a, b) => compareUpcomingKeys(a.key, b.key));
merged.sort((a, b) => compareKeys(a.key, b.key));
// Collapse batch drops: group 3+ episodes from the same title on the same date into one item
const collapsed: (RawItem & { episodeCount: number })[] = [];
@@ -534,7 +571,7 @@ export function getUpcomingFeed(
// Cursor is base64url-encoded UTF-8 JSON {d, n, i, s, e} matching the sort key.
let startIdx = 0;
if (cursorKey) {
startIdx = collapsed.findIndex((item) => compareUpcomingKeys(item.key, cursorKey) > 0);
startIdx = collapsed.findIndex((item) => compareKeys(item.key, cursorKey) > 0);
if (startIdx === -1) startIdx = collapsed.length;
}
+111
View File
@@ -388,3 +388,114 @@ describe("streaming provider", () => {
expect(result.items[0].streamingProvider).toBeNull();
});
});
// ── Recent direction ────────────────────────────────────────────────
describe("recent direction", () => {
test("returns recently aired episodes newest first", () => {
insertTvShow("tv-1", 100, 1, 3, {
title: "Show",
airDates: [daysFromNow(-3), daysFromNow(-1), daysFromNow(-2)],
});
insertStatus("user-1", "tv-1", "in_progress");
const result = getUpcomingFeed("user-1", { direction: "recent" });
expect(result.items.map((i) => i.date)).toEqual([
daysFromNow(-1),
daysFromNow(-2),
daysFromNow(-3),
]);
expect(result.items.map((i) => i.episodeNumber)).toEqual([2, 3, 1]);
});
test("excludes watched episodes", () => {
const { episodeIds } = insertTvShow("tv-1", 100, 1, 2, {
airDates: [daysFromNow(-1), daysFromNow(-2)],
});
insertStatus("user-1", "tv-1", "in_progress");
insertEpisodeWatch("user-1", episodeIds[0]!);
const result = getUpcomingFeed("user-1", { direction: "recent" });
expect(result.items).toHaveLength(1);
expect(result.items[0]!.episodeNumber).toBe(2);
});
test("excludes today's and future episodes", () => {
insertTvShow("tv-1", 100, 1, 3, {
airDates: [daysFromNow(0), daysFromNow(1), daysFromNow(-1)],
});
insertStatus("user-1", "tv-1", "in_progress");
const result = getUpcomingFeed("user-1", { direction: "recent" });
expect(result.items).toHaveLength(1);
expect(result.items[0]!.date).toBe(daysFromNow(-1));
});
test("excludes watchlist titles and includes completed titles", () => {
insertTvShow("tv-w", 101, 1, 1, { title: "Watchlisted", airDates: [daysFromNow(-1)] });
insertTvShow("tv-c", 102, 1, 1, { title: "Completed", airDates: [daysFromNow(-1)] });
insertStatus("user-1", "tv-w", "watchlist");
insertStatus("user-1", "tv-c", "completed");
const result = getUpcomingFeed("user-1", { direction: "recent" });
expect(result.items.map((i) => i.titleName)).toEqual(["Completed"]);
});
test("mediaType movie returns nothing and movies never appear", () => {
insertTitle({ id: "m-1", tmdbId: 500, type: "movie", releaseDate: daysFromNow(-1) });
insertStatus("user-1", "m-1", "completed");
insertTvShow("tv-1", 100, 1, 1, { airDates: [daysFromNow(-1)] });
insertStatus("user-1", "tv-1", "in_progress");
expect(getUpcomingFeed("user-1", { direction: "recent", mediaType: "movie" }).items).toEqual(
[],
);
const all = getUpcomingFeed("user-1", { direction: "recent" });
expect(all.items.every((i) => i.titleType === "tv")).toBe(true);
expect(all.items).toHaveLength(1);
});
test("respects days", () => {
insertTvShow("tv-1", 100, 1, 2, { airDates: [daysFromNow(-7), daysFromNow(-8)] });
insertStatus("user-1", "tv-1", "in_progress");
const result = getUpcomingFeed("user-1", { direction: "recent", days: 7 });
expect(result.items).toHaveLength(1);
expect(result.items[0]!.date).toBe(daysFromNow(-7));
});
test("paginates with a cursor in descending date order", () => {
for (let n = 1; n <= 5; n++) {
insertTvShow(`tv-${n}`, 100 + n, 1, 1, { title: `Show ${n}`, airDates: [daysFromNow(-n)] });
insertStatus("user-1", `tv-${n}`, "in_progress");
}
const page1 = getUpcomingFeed("user-1", { direction: "recent", limit: 2 });
expect(page1.items.map((i) => i.titleName)).toEqual(["Show 1", "Show 2"]);
expect(page1.nextCursor).not.toBeNull();
const page2 = getUpcomingFeed("user-1", {
direction: "recent",
limit: 2,
cursor: page1.nextCursor!,
});
expect(page2.items.map((i) => i.titleName)).toEqual(["Show 3", "Show 4"]);
expect(page2.nextCursor).not.toBeNull();
const page3 = getUpcomingFeed("user-1", {
direction: "recent",
limit: 2,
cursor: page2.nextCursor!,
});
expect(page3.items.map((i) => i.titleName)).toEqual(["Show 5"]);
expect(page3.nextCursor).toBeNull();
});
test("default call still returns only today-and-later items", () => {
insertTvShow("tv-1", 100, 1, 3, {
airDates: [daysFromNow(-1), daysFromNow(0), daysFromNow(1)],
});
insertStatus("user-1", "tv-1", "in_progress");
const result = getUpcomingFeed("user-1", { days: 7 });
expect(result.items.map((i) => i.date)).toEqual([daysFromNow(0), daysFromNow(1)]);
});
});
+47 -1
View File
@@ -1,4 +1,4 @@
import { and, asc, desc, eq, gte, inArray, lte, sql } from "drizzle-orm";
import { and, asc, desc, eq, gte, inArray, lte, notExists, sql } from "drizzle-orm";
import { localDateString } from "@sofa/config";
@@ -310,6 +310,52 @@ export function getUpcomingEpisodes(
.all();
}
/** Aired, unwatched episodes for in-progress/completed titles, newest first. */
export function getRecentUnwatchedEpisodes(userId: string, fromDate: string, toDate: string) {
return db
.select({
episodeId: episodes.id,
titleId: titles.id,
titleName: titles.title,
posterPath: titles.posterPath,
posterThumbHash: titles.posterThumbHash,
backdropPath: titles.backdropPath,
backdropThumbHash: titles.backdropThumbHash,
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),
inArray(userTitleStatus.status, ["in_progress", "completed"]),
notExists(
db
.select({ x: sql`1` })
.from(userEpisodeWatches)
.where(
and(
eq(userEpisodeWatches.episodeId, episodes.id),
eq(userEpisodeWatches.userId, userId),
),
),
),
),
)
.orderBy(desc(episodes.airDate), asc(titles.title))
.all();
}
export function getUpcomingMovies(
userId: string,
fromDate: string,
+15 -2
View File
@@ -46,9 +46,18 @@ function getBucketKey(dateStr: string, today: string): BucketKey {
return `month_${dateStr.slice(0, 7)}`;
}
function getPastBucketKey(dateStr: string, today: string): BucketKey {
if (dateStr === today) return "today";
if (dateStr === addDays(today, -1)) return "yesterday";
if (dateStr >= addDays(today, -6)) return "earlier_this_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 === "yesterday") return i18n._(msg`Yesterday`);
if (key === "earlier_this_week") return i18n._(msg`Earlier this week`);
if (key === "this_week") return i18n._(msg`This Week`);
if (key === "next_week") return i18n._(msg`Next Week`);
if (key.startsWith("month_")) {
@@ -57,13 +66,17 @@ function getBucketLabel(key: BucketKey): string {
return key;
}
export function groupByDateBucket<T extends { date: string }>(items: T[]): DateBucket<T>[] {
export function groupByDateBucket<T extends { date: string }>(
items: T[],
options?: { past?: boolean },
): DateBucket<T>[] {
const bucketKeyFor = options?.past ? getPastBucketKey : getBucketKey;
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);
const key = bucketKeyFor(item.date, today);
let bucket = bucketMap.get(key);
if (!bucket) {
bucket = { label: getBucketLabel(key), items: [] };
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "تنزيل النسخة الاحتياطية"
msgid "Download your library, watch history, and ratings as JSON"
msgstr "تنزيل مكتبتك وسجل المشاهدة والتقييمات بتنسيق JSON"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo"
msgstr "تعديل صورة الملف الشخصي"
@@ -2589,6 +2593,11 @@ msgstr "جاهز — لم يتم استلام أي شيء بعد"
msgid "Recent Searches"
msgstr "عمليات البحث الأخيرة"
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Recently aired"
msgstr ""
#: apps/native/src/components/search/recently-viewed-list.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "إلغاء مشاهدة الكل"
msgid "Unwatched all of {seasonLabel}"
msgstr "تم إلغاء مشاهدة كل {seasonLabel}"
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Unwatched episodes from the last 90 days."
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "تم إلغاء مشاهدة م{seasonNum} ح{epNum}"
@@ -3765,10 +3778,19 @@ msgstr "السنة"
#~ msgid "Year to"
#~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL."
msgstr "سيتم تسجيل خروجك لتغيير رابط الخادم."
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "You're all caught up on the last 90 days."
msgstr ""
#: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}."
#~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "Backup herunterladen"
msgid "Download your library, watch history, and ratings as JSON"
msgstr "Lade deine Bibliothek, den Wiedergabeverlauf und Bewertungen als JSON herunter"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo"
msgstr "Profilfoto bearbeiten"
@@ -2589,6 +2593,11 @@ msgstr "Bereit - noch nichts empfangen"
msgid "Recent Searches"
msgstr "Letzte Suchen"
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Recently aired"
msgstr ""
#: apps/native/src/components/search/recently-viewed-list.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "Alle als ungesehen markieren"
msgid "Unwatched all of {seasonLabel}"
msgstr "Alle von {seasonLabel} als ungesehen markiert"
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Unwatched episodes from the last 90 days."
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "S{seasonNum} E{epNum} als ungesehen markiert"
@@ -3765,10 +3778,19 @@ msgstr "Jahr"
#~ msgid "Year to"
#~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL."
msgstr "Du wirst abgemeldet, um die Server-URL zu ändern."
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "You're all caught up on the last 90 days."
msgstr ""
#: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}."
#~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr ""
msgid "Download your library, watch history, and ratings as JSON"
msgstr "Download your library, watch history, and ratings as JSON"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr "Earlier this week"
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo"
msgstr "Edit profile photo"
@@ -2589,6 +2593,11 @@ msgstr ""
msgid "Recent Searches"
msgstr ""
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Recently aired"
msgstr "Recently aired"
#: apps/native/src/components/search/recently-viewed-list.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr ""
msgid "Unwatched all of {seasonLabel}"
msgstr "Unwatched all of {seasonLabel}"
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Unwatched episodes from the last 90 days."
msgstr "Unwatched episodes from the last 90 days."
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}"
msgstr ""
@@ -3765,10 +3778,19 @@ msgstr "Year"
#~ msgid "Year to"
#~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr "Yesterday"
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL."
msgstr ""
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "You're all caught up on the last 90 days."
msgstr "You're all caught up on the last 90 days."
#: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}."
#~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "Descargar copia de seguridad"
msgid "Download your library, watch history, and ratings as JSON"
msgstr "Descarga tu biblioteca, historial de visualización y valoraciones como JSON"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo"
msgstr "Editar foto de perfil"
@@ -2589,6 +2593,11 @@ msgstr "Listo — nada recibido aún"
msgid "Recent Searches"
msgstr "Búsquedas recientes"
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Recently aired"
msgstr ""
#: apps/native/src/components/search/recently-viewed-list.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "Desmarcar todo"
msgid "Unwatched all of {seasonLabel}"
msgstr "Desmarcado todo de {seasonLabel}"
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Unwatched episodes from the last 90 days."
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "No visto T{seasonNum} E{epNum}"
@@ -3765,10 +3778,19 @@ msgstr "Año"
#~ msgid "Year to"
#~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL."
msgstr "Se cerrará tu sesión para cambiar la URL del servidor."
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "You're all caught up on the last 90 days."
msgstr ""
#: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}."
#~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "Télécharger la sauvegarde"
msgid "Download your library, watch history, and ratings as JSON"
msgstr "Téléchargez votre bibliothèque, votre historique de visionnage et vos notes en JSON"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo"
msgstr "Modifier la photo de profil"
@@ -2589,6 +2593,11 @@ msgstr "Prêt — rien reçu pour l'instant"
msgid "Recent Searches"
msgstr "Recherches récentes"
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Recently aired"
msgstr ""
#: apps/native/src/components/search/recently-viewed-list.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "Tout démarquer"
msgid "Unwatched all of {seasonLabel}"
msgstr "Tout marqué comme non regardé pour {seasonLabel}"
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Unwatched episodes from the last 90 days."
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "Démarqué S{seasonNum} E{epNum}"
@@ -3765,10 +3778,19 @@ msgstr "Année"
#~ msgid "Year to"
#~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL."
msgstr "Vous serez déconnecté pour modifier l'URL du serveur."
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "You're all caught up on the last 90 days."
msgstr ""
#: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}."
#~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "הורד גיבוי"
msgid "Download your library, watch history, and ratings as JSON"
msgstr "הורד את הספרייה, היסטוריית הצפייה והדירוגים שלך כ-JSON"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo"
msgstr "ערוך תמונת פרופיל"
@@ -2589,6 +2593,11 @@ msgstr "מוכן — טרם התקבל דבר"
msgid "Recent Searches"
msgstr "חיפושים אחרונים"
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Recently aired"
msgstr ""
#: apps/native/src/components/search/recently-viewed-list.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "הסר צפייה מהכל"
msgid "Unwatched all of {seasonLabel}"
msgstr "בוטלה צפייה בכל {seasonLabel}"
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Unwatched episodes from the last 90 days."
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "בוטלה צפייה ב-S{seasonNum} E{epNum}"
@@ -3765,10 +3778,19 @@ msgstr "שנה"
#~ msgid "Year to"
#~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL."
msgstr "תתנתק כדי לשנות את כתובת ה-URL של השרת."
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "You're all caught up on the last 90 days."
msgstr ""
#: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}."
#~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "Scarica backup"
msgid "Download your library, watch history, and ratings as JSON"
msgstr "Scarica la tua libreria, cronologia di visione e valutazioni in formato JSON"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo"
msgstr "Modifica foto profilo"
@@ -2589,6 +2593,11 @@ msgstr "Pronto — nessun evento ricevuto ancora"
msgid "Recent Searches"
msgstr "Ricerche recenti"
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Recently aired"
msgstr ""
#: apps/native/src/components/search/recently-viewed-list.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "Rimuovi visione dalla stagione"
msgid "Unwatched all of {seasonLabel}"
msgstr "Rimosso 'visto' da {seasonLabel}"
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Unwatched episodes from the last 90 days."
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "Non visto S{seasonNum} E{epNum}"
@@ -3765,10 +3778,19 @@ msgstr "Anno"
#~ msgid "Year to"
#~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL."
msgstr "Verrai disconnesso per cambiare l'URL del server."
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "You're all caught up on the last 90 days."
msgstr ""
#: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}."
#~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "バックアップをダウンロード"
msgid "Download your library, watch history, and ratings as JSON"
msgstr "ライブラリ、視聴履歴、評価をJSONとしてダウンロード"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo"
msgstr "プロフィール写真を編集"
@@ -2589,6 +2593,11 @@ msgstr "準備完了 — まだ受信していません"
msgid "Recent Searches"
msgstr "最近の検索"
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Recently aired"
msgstr ""
#: apps/native/src/components/search/recently-viewed-list.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "すべて未視聴に戻す"
msgid "Unwatched all of {seasonLabel}"
msgstr "{seasonLabel}のすべてを未視聴にしました"
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Unwatched episodes from the last 90 days."
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "S{seasonNum} E{epNum}を未視聴にしました"
@@ -3765,10 +3778,19 @@ msgstr "年"
#~ msgid "Year to"
#~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL."
msgstr "サーバーURLを変更するためにサインアウトされます。"
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "You're all caught up on the last 90 days."
msgstr ""
#: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}."
#~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "백업 다운로드"
msgid "Download your library, watch history, and ratings as JSON"
msgstr "라이브러리, 시청 기록, 평점을 JSON으로 다운로드"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo"
msgstr "프로필 사진 편집"
@@ -2589,6 +2593,11 @@ msgstr "준비됨 — 아직 수신된 이벤트 없음"
msgid "Recent Searches"
msgstr "최근 검색"
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Recently aired"
msgstr ""
#: apps/native/src/components/search/recently-viewed-list.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "모두 시청 취소"
msgid "Unwatched all of {seasonLabel}"
msgstr "{seasonLabel} 전체 시청 취소"
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Unwatched episodes from the last 90 days."
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "S{seasonNum} E{epNum} 시청 취소됨"
@@ -3765,10 +3778,19 @@ msgstr "연도"
#~ msgid "Year to"
#~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL."
msgstr "서버 URL을 변경하려면 로그아웃됩니다."
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "You're all caught up on the last 90 days."
msgstr ""
#: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}."
#~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "Back-up downloaden"
msgid "Download your library, watch history, and ratings as JSON"
msgstr "Download je bibliotheek, kijkgeschiedenis en beoordelingen als JSON"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo"
msgstr "Profielfoto bewerken"
@@ -2589,6 +2593,11 @@ msgstr "Gereed — nog niets ontvangen"
msgid "Recent Searches"
msgstr "Recente zoekopdrachten"
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Recently aired"
msgstr ""
#: apps/native/src/components/search/recently-viewed-list.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "Alles als onbekeken markeren"
msgid "Unwatched all of {seasonLabel}"
msgstr "Alles van {seasonLabel} als niet bekeken gemarkeerd"
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Unwatched episodes from the last 90 days."
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "Niet bekeken S{seasonNum} E{epNum}"
@@ -3765,10 +3778,19 @@ msgstr "Jaar"
#~ msgid "Year to"
#~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL."
msgstr "Je wordt afgemeld om de server-URL te wijzigen."
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "You're all caught up on the last 90 days."
msgstr ""
#: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}."
#~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "Baixar backup"
msgid "Download your library, watch history, and ratings as JSON"
msgstr "Descarregue a sua biblioteca, histórico de visualizações e avaliações como JSON"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo"
msgstr "Editar foto de perfil"
@@ -2589,6 +2593,11 @@ msgstr "Pronto — nada recebido ainda"
msgid "Recent Searches"
msgstr "Pesquisas Recentes"
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Recently aired"
msgstr ""
#: apps/native/src/components/search/recently-viewed-list.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "Desmarcar todos"
msgid "Unwatched all of {seasonLabel}"
msgstr "Desmarcado tudo de {seasonLabel}"
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Unwatched episodes from the last 90 days."
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "Desmarcado T{seasonNum} E{epNum}"
@@ -3765,10 +3778,19 @@ msgstr "Ano"
#~ msgid "Year to"
#~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL."
msgstr "Você será desconectado para alterar a URL do servidor."
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "You're all caught up on the last 90 days."
msgstr ""
#: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}."
#~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "下载备份"
msgid "Download your library, watch history, and ratings as JSON"
msgstr "将媒体库、观看历史和评分下载为 JSON"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo"
msgstr "编辑头像"
@@ -2589,6 +2593,11 @@ msgstr "就绪 — 尚未收到任何事件"
msgid "Recent Searches"
msgstr "最近搜索"
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Recently aired"
msgstr ""
#: apps/native/src/components/search/recently-viewed-list.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "标记全部为未看"
msgid "Unwatched all of {seasonLabel}"
msgstr "已将 {seasonLabel} 全部标记为未看"
#: apps/web/src/routes/_app/upcoming.tsx
msgid "Unwatched episodes from the last 90 days."
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "第 {seasonNum} 季第 {epNum} 集已取消标记"
@@ -3765,10 +3778,19 @@ msgstr "年份"
#~ msgid "Year to"
#~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL."
msgstr "更改服务器 URL 将使您退出登录。"
#: apps/native/src/app/(tabs)/(home)/upcoming.tsx
#: apps/web/src/routes/_app/upcoming.tsx
msgid "You're all caught up on the last 90 days."
msgstr ""
#: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}."
#~ msgstr ""
+34
View File
@@ -154,3 +154,37 @@ describe("groupByDateBucket", () => {
expect(result[1].label).toBeTruthy();
});
});
describe("groupByDateBucket past option", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2024-06-12T10:00:00"));
});
afterEach(() => {
vi.useRealTimers();
});
test("yesterday goes to the 'yesterday' bucket", () => {
const result = groupByDateBucket([{ date: "2024-06-11" }], { past: true });
expect(result[0].key).toBe("yesterday");
expect(result[0].label).toBe("Yesterday");
});
test("3 days ago goes to 'earlier_this_week'", () => {
const result = groupByDateBucket([{ date: "2024-06-09" }], { past: true });
expect(result[0].key).toBe("earlier_this_week");
expect(result[0].label).toBe("Earlier this week");
});
test("20 days ago goes to a month bucket", () => {
const result = groupByDateBucket([{ date: "2024-05-23" }], { past: true });
expect(result[0].key).toBe("month_2024-05");
expect(result[0].label).toBe("May");
});
test("default grouping is unchanged for a future date", () => {
const result = groupByDateBucket([{ date: "2024-06-13" }]);
expect(result[0].key).toBe("tomorrow");
});
});