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
+619 -26

No files matched your search

+26 -4
View File
@@ -54,6 +54,8 @@ export default function UpcomingScreen() {
const backgroundColor = useCSSVariable("--color-background") as string; const backgroundColor = useCSSVariable("--color-background") as string;
const mutedColor = useCSSVariable("--color-muted-foreground") 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 [mediaType, setMediaType] = useState<"all" | "movie" | "tv">("all");
const [statusFilter, setStatusFilter] = useState<"all" | "watching" | "watchlist">("all"); const [statusFilter, setStatusFilter] = useState<"all" | "watching" | "watchlist">("all");
@@ -64,8 +66,9 @@ export default function UpcomingScreen() {
days: 90, days: 90,
limit: 20, limit: 20,
cursor: pageParam, cursor: pageParam,
mediaType: mediaType !== "all" ? mediaType : undefined, direction: view,
statusFilter: statusFilter !== "all" ? [statusFilter] : undefined, mediaType: !isRecent && mediaType !== "all" ? mediaType : undefined,
statusFilter: !isRecent && statusFilter !== "all" ? [statusFilter] : undefined,
}), }),
initialPageParam: undefined as string | undefined, initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? 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 allItems = useMemo(() => data?.pages.flatMap((p) => p.items) ?? [], [data]);
const sections = useMemo( const sections = useMemo(
() => () =>
groupByDateBucket(allItems).map((b) => ({ groupByDateBucket(allItems, { past: view === "recent" }).map((b) => ({
key: b.key, key: b.key,
title: b.label, title: b.label,
data: b.items, data: b.items,
})), })),
[allItems], [allItems, view],
); );
const refreshUpcoming = useCallback( const refreshUpcoming = useCallback(
@@ -101,6 +104,19 @@ export default function UpcomingScreen() {
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 16, gap: 6, paddingTop: 10, paddingBottom: 6 }} contentContainerStyle={{ paddingHorizontal: 16, gap: 6, paddingTop: 10, paddingBottom: 6 }}
> >
<FilterChip
label={t`Upcoming`}
isSelected={view === "upcoming"}
onPress={() => setView("upcoming")}
/>
<FilterChip
label={t`Recently aired`}
isSelected={view === "recent"}
onPress={() => setView("recent")}
/>
<View className="bg-border/30 mx-1 w-px self-stretch" />
{!isRecent && (
<>
<FilterChip <FilterChip
label={t`All`} label={t`All`}
isSelected={mediaType === "all"} isSelected={mediaType === "all"}
@@ -127,6 +143,8 @@ export default function UpcomingScreen() {
isSelected={statusFilter === "watchlist"} isSelected={statusFilter === "watchlist"}
onPress={() => setStatusFilter(statusFilter === "watchlist" ? "all" : "watchlist")} onPress={() => setStatusFilter(statusFilter === "watchlist" ? "all" : "watchlist")}
/> />
</>
)}
</ScrollView> </ScrollView>
); );
@@ -185,7 +203,11 @@ export default function UpcomingScreen() {
> >
<ScaledIcon icon={IconCalendarEvent} size={48} color={`${mutedColor}66`} /> <ScaledIcon icon={IconCalendarEvent} size={48} color={`${mutedColor}66`} />
<Text className="text-muted-foreground mt-4 text-center text-sm"> <Text className="text-muted-foreground mt-4 text-center text-sm">
{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> <Trans>No upcoming episodes or releases in the next 90 days.</Trans>
)}
</Text> </Text>
</Animated.View> </Animated.View>
) )
@@ -84,6 +84,7 @@ export const upcoming = os.library.upcoming.use(authed).handler(({ input, contex
cursor: input.cursor, cursor: input.cursor,
mediaType: input.mediaType, mediaType: input.mediaType,
statusFilter: input.statusFilter, statusFilter: input.statusFilter,
direction: input.direction,
}); });
return { return {
items: result.items.map((item) => ({ items: result.items.map((item) => ({
+49 -7
View File
@@ -14,6 +14,7 @@ import { orpc } from "@/lib/orpc/client";
import { groupByDateBucket } from "@sofa/i18n/date-buckets"; import { groupByDateBucket } from "@sofa/i18n/date-buckets";
const upcomingSearchSchema = z.object({ const upcomingSearchSchema = z.object({
view: z.enum(["upcoming", "recent"]).optional().catch(undefined),
type: z.enum(["all", "movie", "tv"]).optional().catch(undefined), type: z.enum(["all", "movie", "tv"]).optional().catch(undefined),
status: z.enum(["all", "watching", "watchlist"]).optional().catch(undefined), status: z.enum(["all", "watching", "watchlist"]).optional().catch(undefined),
}); });
@@ -65,6 +66,8 @@ function UpcomingPage() {
const search = Route.useSearch(); const search = Route.useSearch();
const navigate = useNavigate({ from: Route.fullPath }); const navigate = useNavigate({ from: Route.fullPath });
const view = search.view ?? "upcoming";
const isRecent = view === "recent";
const typeFilter = search.type ?? "all"; const typeFilter = search.type ?? "all";
const statusFilter = search.status ?? "all"; const statusFilter = search.status ?? "all";
@@ -75,9 +78,12 @@ function UpcomingPage() {
days: 90, days: 90,
limit: 20, limit: 20,
cursor: pageParam, cursor: pageParam,
mediaType: typeFilter !== "all" ? (typeFilter as "movie" | "tv") : undefined, direction: view,
mediaType: !isRecent && typeFilter !== "all" ? (typeFilter as "movie" | "tv") : undefined,
statusFilter: statusFilter:
statusFilter !== "all" ? [statusFilter as "watching" | "watchlist"] : undefined, !isRecent && statusFilter !== "all"
? [statusFilter as "watching" | "watchlist"]
: undefined,
}), }),
initialPageParam: undefined as string | undefined, initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
@@ -91,6 +97,16 @@ function UpcomingPage() {
isFetchNextPageError, isFetchNextPageError,
}); });
function setView(value: "upcoming" | "recent") {
void navigate({
search: (prev) => ({
...prev,
view: value === "upcoming" ? undefined : value,
}),
replace: true,
});
}
function setTypeFilter(value: string) { function setTypeFilter(value: string) {
void navigate({ void navigate({
search: (prev) => ({ search: (prev) => ({
@@ -112,6 +128,20 @@ function UpcomingPage() {
} }
const filterToggles = ( const filterToggles = (
<div className="space-y-3">
<ToggleGroup
value={[view]}
onValueChange={(values) => {
const next = values.find((v) => v !== view);
if (next === "upcoming" || next === "recent") setView(next);
}}
variant="outline"
size="sm"
>
<ToggleGroupItem value="upcoming">{t`Upcoming`}</ToggleGroupItem>
<ToggleGroupItem value="recent">{t`Recently aired`}</ToggleGroupItem>
</ToggleGroup>
{!isRecent && (
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<ToggleGroup <ToggleGroup
value={[typeFilter]} value={[typeFilter]}
@@ -140,6 +170,8 @@ function UpcomingPage() {
<ToggleGroupItem value="watchlist">{t`Watchlist`}</ToggleGroupItem> <ToggleGroupItem value="watchlist">{t`Watchlist`}</ToggleGroupItem>
</ToggleGroup> </ToggleGroup>
</div> </div>
)}
</div>
); );
if (isPending) return <UpcomingSkeleton />; if (isPending) return <UpcomingSkeleton />;
@@ -149,23 +181,27 @@ function UpcomingPage() {
if (allItems.length === 0) { if (allItems.length === 0) {
return ( return (
<div className="mx-auto max-w-2xl space-y-6"> <div className="mx-auto max-w-2xl space-y-6">
<UpcomingHeader /> <UpcomingHeader view={view} />
{filterToggles} {filterToggles}
<div className="flex flex-col items-center justify-center py-20 text-center"> <div className="flex flex-col items-center justify-center py-20 text-center">
<IconCalendarEvent className="text-muted-foreground/40 size-12" /> <IconCalendarEvent className="text-muted-foreground/40 size-12" />
<p className="text-muted-foreground mt-4 text-sm"> <p className="text-muted-foreground mt-4 text-sm">
{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> <Trans>No upcoming episodes or releases in the next 90 days.</Trans>
)}
</p> </p>
</div> </div>
</div> </div>
); );
} }
const buckets = groupByDateBucket(allItems); const buckets = groupByDateBucket(allItems, { past: isRecent });
return ( return (
<div className="mx-auto max-w-2xl space-y-6"> <div className="mx-auto max-w-2xl space-y-6">
<UpcomingHeader /> <UpcomingHeader view={view} />
{filterToggles} {filterToggles}
{buckets.map((bucket) => ( {buckets.map((bucket) => (
<section key={bucket.key}> <section key={bucket.key}>
@@ -185,13 +221,19 @@ function UpcomingPage() {
); );
} }
function UpcomingHeader() { function UpcomingHeader({ view }: { view: "upcoming" | "recent" }) {
const { t } = useLingui(); const { t } = useLingui();
return ( return (
<div> <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"> <p className="text-muted-foreground mt-1 text-sm">
{view === "recent" ? (
<Trans>Unwatched episodes from the last 90 days.</Trans>
) : (
<Trans>Episodes and movies coming up in the next 90 days.</Trans> <Trans>Episodes and movies coming up in the next 90 days.</Trans>
)}
</p> </p>
</div> </div>
); );
@@ -10,8 +10,10 @@ _openapi:
structuredData: structuredData:
headings: [] headings: []
contents: contents:
- content: Fetch upcoming episodes and movie releases for titles in the user's - content: 'Fetch upcoming episodes and movie releases for titles in the user''s
library, sorted by date. Supports cursor-based pagination. 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. */} {/* 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": { "get": {
"operationId": "library.upcoming", "operationId": "library.upcoming",
"summary": "Get upcoming episodes and movies", "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": [ "tags": [
"Library" "Library"
], ],
@@ -2929,7 +2929,22 @@
"minimum": 1, "minimum": 1,
"maximum": 90, "maximum": 90,
"default": 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, "allowEmptyValue": true,
"allowReserved": true "allowReserved": true
+1 -1
View File
@@ -238,7 +238,7 @@ export const contract = {
tags: ["Library"], tags: ["Library"],
summary: "Get upcoming episodes and movies", summary: "Get upcoming episodes and movies",
description: 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", successDescription: "Upcoming items sorted by date with streaming info",
}) })
.input(UpcomingInput) .input(UpcomingInput)
+7 -1
View File
@@ -663,7 +663,13 @@ export const UpcomingInput = z
.min(1) .min(1)
.max(90) .max(90)
.default(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"), limit: z.number().int().min(1).max(50).default(20).describe("Maximum items per page"),
cursor: z.string().optional().describe("Pagination cursor"), cursor: z.string().optional().describe("Pagination cursor"),
mediaType: z mediaType: z
+43 -6
View File
@@ -17,6 +17,7 @@ import {
getTitleByIdOrNull, getTitleByIdOrNull,
getTitlesByIds, getTitlesByIds,
getTvTitlesByIds, getTvTitlesByIds,
getRecentUnwatchedEpisodes,
getUpcomingEpisodes, getUpcomingEpisodes,
getUpcomingMovies, getUpcomingMovies,
getUserStatusCounts, 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 { function encodeUpcomingCursor(key: UpcomingCursorKey): string {
return Buffer.from(JSON.stringify(key), "utf8").toString("base64url"); return Buffer.from(JSON.stringify(key), "utf8").toString("base64url");
} }
@@ -435,27 +447,52 @@ export function getUpcomingFeed(
cursor?: string; cursor?: string;
mediaType?: "movie" | "tv"; mediaType?: "movie" | "tv";
statusFilter?: ("watching" | "watchlist")[]; statusFilter?: ("watching" | "watchlist")[];
direction?: "upcoming" | "recent";
} = {}, } = {},
): UpcomingFeedResult { ): 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 // Map display status filter to stored statuses for DB query
const storedStatuses = statusFilter?.map((s) => (s === "watching" ? "in_progress" : s)); const storedStatuses = statusFilter?.map((s) => (s === "watching" ? "in_progress" : s));
const now = new Date(); const now = new Date();
const today = localDateString(now); const today = localDateString(now);
const cursorKey = cursor ? decodeUpcomingCursor(cursor) : null;
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(); const horizon = new Date();
horizon.setDate(horizon.getDate() + days); horizon.setDate(horizon.getDate() + days);
const toDate = localDateString(horizon); const toDate = localDateString(horizon);
// Use the cursor date as the lower bound so later pages skip already-seen dates, // 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. // 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 fromDate = cursorKey?.d ?? today;
const episodeRows = episodeRows =
mediaType === "movie" ? [] : getUpcomingEpisodes(userId, fromDate, toDate, storedStatuses); mediaType === "movie" ? [] : getUpcomingEpisodes(userId, fromDate, toDate, storedStatuses);
const movieRows = movieRows =
mediaType === "tv" ? [] : getUpcomingMovies(userId, fromDate, toDate, storedStatuses); mediaType === "tv" ? [] : getUpcomingMovies(userId, fromDate, toDate, storedStatuses);
}
// Merge into unified items // Merge into unified items
type RawItem = { type RawItem = {
@@ -494,7 +531,7 @@ export function getUpcomingFeed(
]; ];
// Sort by (date, title, titleId, season, episode) ASC — the same key the cursor uses // 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 // Collapse batch drops: group 3+ episodes from the same title on the same date into one item
const collapsed: (RawItem & { episodeCount: number })[] = []; 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. // Cursor is base64url-encoded UTF-8 JSON {d, n, i, s, e} matching the sort key.
let startIdx = 0; let startIdx = 0;
if (cursorKey) { 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; if (startIdx === -1) startIdx = collapsed.length;
} }
+111
View File
@@ -388,3 +388,114 @@ describe("streaming provider", () => {
expect(result.items[0].streamingProvider).toBeNull(); 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"; import { localDateString } from "@sofa/config";
@@ -310,6 +310,52 @@ export function getUpcomingEpisodes(
.all(); .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( export function getUpcomingMovies(
userId: string, userId: string,
fromDate: string, fromDate: string,
+15 -2
View File
@@ -46,9 +46,18 @@ function getBucketKey(dateStr: string, today: string): BucketKey {
return `month_${dateStr.slice(0, 7)}`; 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 { function getBucketLabel(key: BucketKey): string {
if (key === "today") return i18n._(msg`Today`); if (key === "today") return i18n._(msg`Today`);
if (key === "tomorrow") return i18n._(msg`Tomorrow`); 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 === "this_week") return i18n._(msg`This Week`);
if (key === "next_week") return i18n._(msg`Next Week`); if (key === "next_week") return i18n._(msg`Next Week`);
if (key.startsWith("month_")) { if (key.startsWith("month_")) {
@@ -57,13 +66,17 @@ function getBucketLabel(key: BucketKey): string {
return key; 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 today = getToday();
const bucketMap = new Map<string, { label: string; items: T[] }>(); const bucketMap = new Map<string, { label: string; items: T[] }>();
const bucketOrder: string[] = []; const bucketOrder: string[] = [];
for (const item of items) { for (const item of items) {
const key = getBucketKey(item.date, today); const key = bucketKeyFor(item.date, today);
let bucket = bucketMap.get(key); let bucket = bucketMap.get(key);
if (!bucket) { if (!bucket) {
bucket = { label: getBucketLabel(key), items: [] }; bucket = { label: getBucketLabel(key), items: [] };
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "تنزيل النسخة الاحتياطية"
msgid "Download your library, watch history, and ratings as JSON" msgid "Download your library, watch history, and ratings as JSON"
msgstr "تنزيل مكتبتك وسجل المشاهدة والتقييمات بتنسيق JSON" msgstr "تنزيل مكتبتك وسجل المشاهدة والتقييمات بتنسيق JSON"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo" msgid "Edit profile photo"
msgstr "تعديل صورة الملف الشخصي" msgstr "تعديل صورة الملف الشخصي"
@@ -2589,6 +2593,11 @@ msgstr "جاهز — لم يتم استلام أي شيء بعد"
msgid "Recent Searches" msgid "Recent Searches"
msgstr "عمليات البحث الأخيرة" 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.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx #: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed" msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "إلغاء مشاهدة الكل"
msgid "Unwatched all of {seasonLabel}" msgid "Unwatched all of {seasonLabel}"
msgstr "تم إلغاء مشاهدة كل {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 #: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}" msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "تم إلغاء مشاهدة م{seasonNum} ح{epNum}" msgstr "تم إلغاء مشاهدة م{seasonNum} ح{epNum}"
@@ -3765,10 +3778,19 @@ msgstr "السنة"
#~ msgid "Year to" #~ msgid "Year to"
#~ msgstr "Year to" #~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL." msgid "You'll be signed out to change the server URL."
msgstr "سيتم تسجيل خروجك لتغيير رابط الخادم." 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 #: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}." #~ msgid "You're running v{0}."
#~ msgstr "" #~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "Backup herunterladen"
msgid "Download your library, watch history, and ratings as JSON" msgid "Download your library, watch history, and ratings as JSON"
msgstr "Lade deine Bibliothek, den Wiedergabeverlauf und Bewertungen als JSON herunter" 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 #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo" msgid "Edit profile photo"
msgstr "Profilfoto bearbeiten" msgstr "Profilfoto bearbeiten"
@@ -2589,6 +2593,11 @@ msgstr "Bereit - noch nichts empfangen"
msgid "Recent Searches" msgid "Recent Searches"
msgstr "Letzte Suchen" 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.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx #: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed" msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "Alle als ungesehen markieren"
msgid "Unwatched all of {seasonLabel}" msgid "Unwatched all of {seasonLabel}"
msgstr "Alle von {seasonLabel} als ungesehen markiert" 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 #: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}" msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "S{seasonNum} E{epNum} als ungesehen markiert" msgstr "S{seasonNum} E{epNum} als ungesehen markiert"
@@ -3765,10 +3778,19 @@ msgstr "Jahr"
#~ msgid "Year to" #~ msgid "Year to"
#~ msgstr "Year to" #~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL." msgid "You'll be signed out to change the server URL."
msgstr "Du wirst abgemeldet, um die Server-URL zu ändern." 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 #: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}." #~ msgid "You're running v{0}."
#~ msgstr "" #~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr ""
msgid "Download your library, watch history, and ratings as JSON" msgid "Download your library, watch history, and ratings as JSON"
msgstr "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 #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo" msgid "Edit profile photo"
msgstr "Edit profile photo" msgstr "Edit profile photo"
@@ -2589,6 +2593,11 @@ msgstr ""
msgid "Recent Searches" msgid "Recent Searches"
msgstr "" 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.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx #: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed" msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr ""
msgid "Unwatched all of {seasonLabel}" msgid "Unwatched all of {seasonLabel}"
msgstr "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 #: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}" msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "" msgstr ""
@@ -3765,10 +3778,19 @@ msgstr "Year"
#~ msgid "Year to" #~ msgid "Year to"
#~ msgstr "Year to" #~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr "Yesterday"
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL." msgid "You'll be signed out to change the server URL."
msgstr "" 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 #: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}." #~ msgid "You're running v{0}."
#~ msgstr "" #~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "Descargar copia de seguridad"
msgid "Download your library, watch history, and ratings as JSON" msgid "Download your library, watch history, and ratings as JSON"
msgstr "Descarga tu biblioteca, historial de visualización y valoraciones como 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 #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo" msgid "Edit profile photo"
msgstr "Editar foto de perfil" msgstr "Editar foto de perfil"
@@ -2589,6 +2593,11 @@ msgstr "Listo — nada recibido aún"
msgid "Recent Searches" msgid "Recent Searches"
msgstr "Búsquedas recientes" 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.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx #: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed" msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "Desmarcar todo"
msgid "Unwatched all of {seasonLabel}" msgid "Unwatched all of {seasonLabel}"
msgstr "Desmarcado todo de {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 #: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}" msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "No visto T{seasonNum} E{epNum}" msgstr "No visto T{seasonNum} E{epNum}"
@@ -3765,10 +3778,19 @@ msgstr "Año"
#~ msgid "Year to" #~ msgid "Year to"
#~ msgstr "Year to" #~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL." msgid "You'll be signed out to change the server URL."
msgstr "Se cerrará tu sesión para cambiar la URL del servidor." 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 #: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}." #~ msgid "You're running v{0}."
#~ msgstr "" #~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "Télécharger la sauvegarde"
msgid "Download your library, watch history, and ratings as JSON" 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" 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 #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo" msgid "Edit profile photo"
msgstr "Modifier la photo de profil" msgstr "Modifier la photo de profil"
@@ -2589,6 +2593,11 @@ msgstr "Prêt — rien reçu pour l'instant"
msgid "Recent Searches" msgid "Recent Searches"
msgstr "Recherches récentes" 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.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx #: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed" msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "Tout démarquer"
msgid "Unwatched all of {seasonLabel}" msgid "Unwatched all of {seasonLabel}"
msgstr "Tout marqué comme non regardé pour {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 #: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}" msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "Démarqué S{seasonNum} E{epNum}" msgstr "Démarqué S{seasonNum} E{epNum}"
@@ -3765,10 +3778,19 @@ msgstr "Année"
#~ msgid "Year to" #~ msgid "Year to"
#~ msgstr "Year to" #~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL." msgid "You'll be signed out to change the server URL."
msgstr "Vous serez déconnecté pour modifier l'URL du serveur." 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 #: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}." #~ msgid "You're running v{0}."
#~ msgstr "" #~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "הורד גיבוי"
msgid "Download your library, watch history, and ratings as JSON" msgid "Download your library, watch history, and ratings as JSON"
msgstr "הורד את הספרייה, היסטוריית הצפייה והדירוגים שלך כ-JSON" msgstr "הורד את הספרייה, היסטוריית הצפייה והדירוגים שלך כ-JSON"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo" msgid "Edit profile photo"
msgstr "ערוך תמונת פרופיל" msgstr "ערוך תמונת פרופיל"
@@ -2589,6 +2593,11 @@ msgstr "מוכן — טרם התקבל דבר"
msgid "Recent Searches" msgid "Recent Searches"
msgstr "חיפושים אחרונים" 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.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx #: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed" msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "הסר צפייה מהכל"
msgid "Unwatched all of {seasonLabel}" msgid "Unwatched all of {seasonLabel}"
msgstr "בוטלה צפייה בכל {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 #: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}" msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "בוטלה צפייה ב-S{seasonNum} E{epNum}" msgstr "בוטלה צפייה ב-S{seasonNum} E{epNum}"
@@ -3765,10 +3778,19 @@ msgstr "שנה"
#~ msgid "Year to" #~ msgid "Year to"
#~ msgstr "Year to" #~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL." msgid "You'll be signed out to change the server URL."
msgstr "תתנתק כדי לשנות את כתובת ה-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 #: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}." #~ msgid "You're running v{0}."
#~ msgstr "" #~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "Scarica backup"
msgid "Download your library, watch history, and ratings as JSON" msgid "Download your library, watch history, and ratings as JSON"
msgstr "Scarica la tua libreria, cronologia di visione e valutazioni in formato 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 #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo" msgid "Edit profile photo"
msgstr "Modifica foto profilo" msgstr "Modifica foto profilo"
@@ -2589,6 +2593,11 @@ msgstr "Pronto — nessun evento ricevuto ancora"
msgid "Recent Searches" msgid "Recent Searches"
msgstr "Ricerche recenti" 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.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx #: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed" msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "Rimuovi visione dalla stagione"
msgid "Unwatched all of {seasonLabel}" msgid "Unwatched all of {seasonLabel}"
msgstr "Rimosso 'visto' da {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 #: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}" msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "Non visto S{seasonNum} E{epNum}" msgstr "Non visto S{seasonNum} E{epNum}"
@@ -3765,10 +3778,19 @@ msgstr "Anno"
#~ msgid "Year to" #~ msgid "Year to"
#~ msgstr "Year to" #~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL." msgid "You'll be signed out to change the server URL."
msgstr "Verrai disconnesso per cambiare l'URL del server." 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 #: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}." #~ msgid "You're running v{0}."
#~ msgstr "" #~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "バックアップをダウンロード"
msgid "Download your library, watch history, and ratings as JSON" msgid "Download your library, watch history, and ratings as JSON"
msgstr "ライブラリ、視聴履歴、評価をJSONとしてダウンロード" msgstr "ライブラリ、視聴履歴、評価をJSONとしてダウンロード"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo" msgid "Edit profile photo"
msgstr "プロフィール写真を編集" msgstr "プロフィール写真を編集"
@@ -2589,6 +2593,11 @@ msgstr "準備完了 — まだ受信していません"
msgid "Recent Searches" msgid "Recent Searches"
msgstr "最近の検索" 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.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx #: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed" msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "すべて未視聴に戻す"
msgid "Unwatched all of {seasonLabel}" msgid "Unwatched all of {seasonLabel}"
msgstr "{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 #: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}" msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "S{seasonNum} E{epNum}を未視聴にしました" msgstr "S{seasonNum} E{epNum}を未視聴にしました"
@@ -3765,10 +3778,19 @@ msgstr "年"
#~ msgid "Year to" #~ msgid "Year to"
#~ msgstr "Year to" #~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL." msgid "You'll be signed out to change the server URL."
msgstr "サーバー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 #: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}." #~ msgid "You're running v{0}."
#~ msgstr "" #~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "백업 다운로드"
msgid "Download your library, watch history, and ratings as JSON" msgid "Download your library, watch history, and ratings as JSON"
msgstr "라이브러리, 시청 기록, 평점을 JSON으로 다운로드" msgstr "라이브러리, 시청 기록, 평점을 JSON으로 다운로드"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo" msgid "Edit profile photo"
msgstr "프로필 사진 편집" msgstr "프로필 사진 편집"
@@ -2589,6 +2593,11 @@ msgstr "준비됨 — 아직 수신된 이벤트 없음"
msgid "Recent Searches" msgid "Recent Searches"
msgstr "최근 검색" 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.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx #: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed" msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "모두 시청 취소"
msgid "Unwatched all of {seasonLabel}" msgid "Unwatched all of {seasonLabel}"
msgstr "{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 #: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}" msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "S{seasonNum} E{epNum} 시청 취소됨" msgstr "S{seasonNum} E{epNum} 시청 취소됨"
@@ -3765,10 +3778,19 @@ msgstr "연도"
#~ msgid "Year to" #~ msgid "Year to"
#~ msgstr "Year to" #~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL." msgid "You'll be signed out to change the server URL."
msgstr "서버 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 #: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}." #~ msgid "You're running v{0}."
#~ msgstr "" #~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "Back-up downloaden"
msgid "Download your library, watch history, and ratings as JSON" msgid "Download your library, watch history, and ratings as JSON"
msgstr "Download je bibliotheek, kijkgeschiedenis en beoordelingen als 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 #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo" msgid "Edit profile photo"
msgstr "Profielfoto bewerken" msgstr "Profielfoto bewerken"
@@ -2589,6 +2593,11 @@ msgstr "Gereed — nog niets ontvangen"
msgid "Recent Searches" msgid "Recent Searches"
msgstr "Recente zoekopdrachten" 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.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx #: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed" msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "Alles als onbekeken markeren"
msgid "Unwatched all of {seasonLabel}" msgid "Unwatched all of {seasonLabel}"
msgstr "Alles van {seasonLabel} als niet bekeken gemarkeerd" 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 #: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}" msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "Niet bekeken S{seasonNum} E{epNum}" msgstr "Niet bekeken S{seasonNum} E{epNum}"
@@ -3765,10 +3778,19 @@ msgstr "Jaar"
#~ msgid "Year to" #~ msgid "Year to"
#~ msgstr "Year to" #~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL." msgid "You'll be signed out to change the server URL."
msgstr "Je wordt afgemeld om de server-URL te wijzigen." 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 #: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}." #~ msgid "You're running v{0}."
#~ msgstr "" #~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "Baixar backup"
msgid "Download your library, watch history, and ratings as JSON" 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" 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 #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo" msgid "Edit profile photo"
msgstr "Editar foto de perfil" msgstr "Editar foto de perfil"
@@ -2589,6 +2593,11 @@ msgstr "Pronto — nada recebido ainda"
msgid "Recent Searches" msgid "Recent Searches"
msgstr "Pesquisas Recentes" 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.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx #: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed" msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "Desmarcar todos"
msgid "Unwatched all of {seasonLabel}" msgid "Unwatched all of {seasonLabel}"
msgstr "Desmarcado tudo de {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 #: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}" msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "Desmarcado T{seasonNum} E{epNum}" msgstr "Desmarcado T{seasonNum} E{epNum}"
@@ -3765,10 +3778,19 @@ msgstr "Ano"
#~ msgid "Year to" #~ msgid "Year to"
#~ msgstr "Year to" #~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL." msgid "You'll be signed out to change the server URL."
msgstr "Você será desconectado para alterar a URL do servidor." 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 #: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}." #~ msgid "You're running v{0}."
#~ msgstr "" #~ msgstr ""
+22
View File
@@ -1071,6 +1071,10 @@ msgstr "下载备份"
msgid "Download your library, watch history, and ratings as JSON" msgid "Download your library, watch history, and ratings as JSON"
msgstr "将媒体库、观看历史和评分下载为 JSON" msgstr "将媒体库、观看历史和评分下载为 JSON"
#: packages/i18n/src/date-buckets.ts
msgid "Earlier this week"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Edit profile photo" msgid "Edit profile photo"
msgstr "编辑头像" msgstr "编辑头像"
@@ -2589,6 +2593,11 @@ msgstr "就绪 — 尚未收到任何事件"
msgid "Recent Searches" msgid "Recent Searches"
msgstr "最近搜索" 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.ios.tsx
#: apps/native/src/components/search/recently-viewed-list.tsx #: apps/native/src/components/search/recently-viewed-list.tsx
msgid "Recently Viewed" msgid "Recently Viewed"
@@ -3517,6 +3526,10 @@ msgstr "标记全部为未看"
msgid "Unwatched all of {seasonLabel}" msgid "Unwatched all of {seasonLabel}"
msgstr "已将 {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 #: apps/web/src/components/titles/use-title-actions.ts
msgid "Unwatched S{seasonNum} E{epNum}" msgid "Unwatched S{seasonNum} E{epNum}"
msgstr "第 {seasonNum} 季第 {epNum} 集已取消标记" msgstr "第 {seasonNum} 季第 {epNum} 集已取消标记"
@@ -3765,10 +3778,19 @@ msgstr "年份"
#~ msgid "Year to" #~ msgid "Year to"
#~ msgstr "Year to" #~ msgstr "Year to"
#: packages/i18n/src/date-buckets.ts
msgid "Yesterday"
msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx #: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "You'll be signed out to change the server URL." msgid "You'll be signed out to change the server URL."
msgstr "更改服务器 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 #: apps/web/src/components/update-toast.tsx
#~ msgid "You're running v{0}." #~ msgid "You're running v{0}."
#~ msgstr "" #~ msgstr ""
+34
View File
@@ -154,3 +154,37 @@ describe("groupByDateBucket", () => {
expect(result[1].label).toBeTruthy(); 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");
});
});