mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-16 01:05:42 -04:00
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:
24 files changed
+671
-78
No files matched your search
@@ -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(
|
||||||
@@ -102,31 +105,46 @@ export default function UpcomingScreen() {
|
|||||||
contentContainerStyle={{ paddingHorizontal: 16, gap: 6, paddingTop: 10, paddingBottom: 6 }}
|
contentContainerStyle={{ paddingHorizontal: 16, gap: 6, paddingTop: 10, paddingBottom: 6 }}
|
||||||
>
|
>
|
||||||
<FilterChip
|
<FilterChip
|
||||||
label={t`All`}
|
label={t`Upcoming`}
|
||||||
isSelected={mediaType === "all"}
|
isSelected={view === "upcoming"}
|
||||||
onPress={() => setMediaType("all")}
|
onPress={() => setView("upcoming")}
|
||||||
/>
|
/>
|
||||||
<FilterChip
|
<FilterChip
|
||||||
label={t`Movies`}
|
label={t`Recently aired`}
|
||||||
isSelected={mediaType === "movie"}
|
isSelected={view === "recent"}
|
||||||
onPress={() => setMediaType("movie")}
|
onPress={() => setView("recent")}
|
||||||
/>
|
|
||||||
<FilterChip
|
|
||||||
label={t`TV`}
|
|
||||||
isSelected={mediaType === "tv"}
|
|
||||||
onPress={() => setMediaType("tv")}
|
|
||||||
/>
|
/>
|
||||||
<View className="bg-border/30 mx-1 w-px self-stretch" />
|
<View className="bg-border/30 mx-1 w-px self-stretch" />
|
||||||
<FilterChip
|
{!isRecent && (
|
||||||
label={t`Watching`}
|
<>
|
||||||
isSelected={statusFilter === "watching"}
|
<FilterChip
|
||||||
onPress={() => setStatusFilter(statusFilter === "watching" ? "all" : "watching")}
|
label={t`All`}
|
||||||
/>
|
isSelected={mediaType === "all"}
|
||||||
<FilterChip
|
onPress={() => setMediaType("all")}
|
||||||
label={t`Watchlist`}
|
/>
|
||||||
isSelected={statusFilter === "watchlist"}
|
<FilterChip
|
||||||
onPress={() => setStatusFilter(statusFilter === "watchlist" ? "all" : "watchlist")}
|
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>
|
</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">
|
||||||
<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>
|
</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) => ({
|
||||||
|
|||||||
@@ -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,33 +128,49 @@ function UpcomingPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filterToggles = (
|
const filterToggles = (
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="space-y-3">
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
value={[typeFilter]}
|
value={[view]}
|
||||||
onValueChange={(values) => {
|
onValueChange={(values) => {
|
||||||
const next = values.find((v) => v !== typeFilter) ?? "all";
|
const next = values.find((v) => v !== view);
|
||||||
setTypeFilter(next);
|
if (next === "upcoming" || next === "recent") setView(next);
|
||||||
}}
|
}}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<ToggleGroupItem value="all">{t`All`}</ToggleGroupItem>
|
<ToggleGroupItem value="upcoming">{t`Upcoming`}</ToggleGroupItem>
|
||||||
<ToggleGroupItem value="movie">{t`Movies`}</ToggleGroupItem>
|
<ToggleGroupItem value="recent">{t`Recently aired`}</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>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -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">
|
||||||
<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>
|
</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">
|
||||||
<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>
|
</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. */}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 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 cursorKey = cursor ? decodeUpcomingCursor(cursor) : null;
|
||||||
const fromDate = cursorKey?.d ?? today;
|
|
||||||
const episodeRows =
|
let episodeRows: ReturnType<typeof getUpcomingEpisodes>;
|
||||||
mediaType === "movie" ? [] : getUpcomingEpisodes(userId, fromDate, toDate, storedStatuses);
|
let movieRows: ReturnType<typeof getUpcomingMovies>;
|
||||||
const movieRows =
|
if (isRecent) {
|
||||||
mediaType === "tv" ? [] : getUpcomingMovies(userId, fromDate, toDate, storedStatuses);
|
// 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
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: [] };
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user