diff --git a/api/src/models/entry/episode.ts b/api/src/models/entry/episode.ts index 1c4bb99b..707e9fec 100644 --- a/api/src/models/entry/episode.ts +++ b/api/src/models/entry/episode.ts @@ -15,7 +15,7 @@ import { BaseEntry, EntryTranslation } from "./base-entry"; export const BaseEpisode = t.Composite([ t.Object({ kind: t.Literal("episode"), - order: t.Number({ minimum: 1, description: "Absolute playback order." }), + order: t.Number({ description: "Absolute playback order." }), seasonNumber: t.Integer(), episodeNumber: t.Integer(), externalId: EpisodeId, diff --git a/api/src/models/entry/movie-entry.ts b/api/src/models/entry/movie-entry.ts index 9432b483..d7ca4fc2 100644 --- a/api/src/models/entry/movie-entry.ts +++ b/api/src/models/entry/movie-entry.ts @@ -18,7 +18,6 @@ export const BaseMovieEntry = t.Composite( t.Object({ kind: t.Literal("movie"), order: t.Number({ - minimum: 1, description: "Absolute playback order. Can be mixed with episodes.", }), externalId: ExternalId(), diff --git a/api/src/models/entry/special.ts b/api/src/models/entry/special.ts index 97a65cae..121940e3 100644 --- a/api/src/models/entry/special.ts +++ b/api/src/models/entry/special.ts @@ -17,10 +17,9 @@ export const BaseSpecial = t.Composite( t.Object({ kind: t.Literal("special"), order: t.Number({ - minimum: 1, description: "Absolute playback order. Can be mixed with episodes.", }), - number: t.Integer({ minimum: 1 }), + number: t.Integer(), externalId: EpisodeId, }), BaseEntry(), diff --git a/front/src/components/entries/entry-line.tsx b/front/src/components/entries/entry-line.tsx index 9a5cbd05..b3f569ee 100644 --- a/front/src/components/entries/entry-line.tsx +++ b/front/src/components/entries/entry-line.tsx @@ -47,7 +47,7 @@ export const EntryLine = ({ airDate: Date | null; runtime: number | null; watchedPercent: number | null; - href: string; + href: string | null; } & PressableProps) => { const [moreOpened, setMoreOpened] = useState(false); const [descriptionExpanded, setDescriptionExpanded] = useState(false); diff --git a/front/src/models/season.ts b/front/src/models/season.ts index a3cf5962..1640b0f3 100644 --- a/front/src/models/season.ts +++ b/front/src/models/season.ts @@ -8,7 +8,7 @@ export const Season = z.object({ seasonNumber: z.int().gte(0), name: z.string().nullable(), description: z.string().nullable(), - entryCount: z.int().gte(0), + entriesCount: z.int().gte(0), availableCount: z.int().gte(0), startAir: zdate().nullable(), endAir: zdate().nullable(), diff --git a/front/src/query/fetch-infinite.tsx b/front/src/query/fetch-infinite.tsx index 01a7a234..e21a9ce7 100644 --- a/front/src/query/fetch-infinite.tsx +++ b/front/src/query/fetch-infinite.tsx @@ -12,7 +12,7 @@ export type Layout = { layout: "grid" | "horizontal" | "vertical"; }; -export const InfiniteFetch = ({ +export const InfiniteFetch = ({ query, placeholderCount = 2, incremental = false, @@ -34,7 +34,7 @@ export const InfiniteFetch = ({ Empty?: JSX.Element; incremental?: boolean; divider?: true | ComponentType; - Header?: ComponentType | ReactElement; + Header?: ComponentType<{ children: JSX.Element }> | ReactElement; fetchMore?: boolean; }): JSX.Element | null => { const { numColumns, size, gap } = useBreakpointMap(layout); diff --git a/front/src/query/fetch.tsx b/front/src/query/fetch.tsx index a6718bc6..bab4aad2 100644 --- a/front/src/query/fetch.tsx +++ b/front/src/query/fetch.tsx @@ -9,8 +9,8 @@ export const Fetch = ({ Loader, }: { query: QueryIdentifier; - Render: (item: Data) => ReactElement; - Loader: () => ReactElement; + Render: (item: Data) => ReactElement | null; + Loader: () => ReactElement | null; }): JSX.Element | null => { const { data, isPaused, error } = useFetch(query); const [setError] = useSetError("fetch"); diff --git a/front/src/ui/details/header.tsx b/front/src/ui/details/header.tsx index 36998630..c2875a94 100644 --- a/front/src/ui/details/header.tsx +++ b/front/src/ui/details/header.tsx @@ -826,6 +826,6 @@ Header.query = ( parser: Show, path: ["api", `${kind}s`, slug], params: { - with: ["studios"], + with: ["studios", ...(kind === "serie" ? ["firstEntry", "nextEntry"] : [])], }, }); diff --git a/front/src/ui/details/season.tsx b/front/src/ui/details/season.tsx index f9a5fbd7..cac2d736 100644 --- a/front/src/ui/details/season.tsx +++ b/front/src/ui/details/season.tsx @@ -67,7 +67,7 @@ export const SeasonHeader = ({ key={x.seasonNumber} label={`${x.seasonNumber}: ${ x.name ?? t("show.season", { number: x.seasonNumber }) - } (${x.entryCount})`} + } (${x.entriesCount})`} href={`/series/${serieSlug}?season=${x.seasonNumber}`} /> ))} @@ -115,8 +115,8 @@ SeasonHeader.query = (slug: string): QueryIdentifier => ({ parser: Season, path: ["api", "series", slug, "seasons"], params: { - // Fetch all seasons at one, there won't be hundred of them anyways. - limit: 0, + // I don't wanna deal with pagination, no serie has more than 100 seasons anyways, right? + limit: 100, }, infinite: true, }); @@ -128,7 +128,7 @@ export const EntryList = ({ }: { slug: string; season: string | number; -} & Partial>) => { +} & Partial>>) => { const { t } = useTranslation(); const { items: seasons, error } = useInfiniteFetch(SeasonHeader.query(slug)); diff --git a/front/src/ui/details/serie.tsx b/front/src/ui/details/serie.tsx index 6d3fa55d..4f6b9bf9 100644 --- a/front/src/ui/details/serie.tsx +++ b/front/src/ui/details/serie.tsx @@ -4,7 +4,9 @@ import { Platform, View } from "react-native"; import Svg, { Path, type SvgProps } from "react-native-svg"; import { percent, useYoshiki } from "yoshiki/native"; import { EntryLine, entryDisplayNumber } from "~/components/entries"; +import type { Entry, Serie } from "~/models"; import { Container, focusReset, H2, SwitchVariant, ts } from "~/primitives"; +import { Fetch } from "~/query"; import { useQueryState } from "~/utils"; import { Header } from "./header"; import { EntryList } from "./season"; @@ -29,15 +31,10 @@ export const SvgWave = (props: SvgProps) => { ); }; -export const ShowWatchStatusCard = ({ - watchedPercent, - nextEpisode, -}: ShowWatchStatus) => { +export const NextUp = (nextEntry: Entry) => { const { t } = useTranslation(); const [focused, setFocus] = useState(false); - if (!nextEpisode) return null; - return ( {({ css }) => ( @@ -60,10 +57,10 @@ export const ShowWatchStatusCard = ({ >

{t("show.nextUp")}

setFocus(true)} onHoverOut={() => setFocus(false)} onFocus={() => setFocus(true)} @@ -75,6 +72,31 @@ export const ShowWatchStatusCard = ({ ); }; +NextUp.Loader = () => { + const { t } = useTranslation(); + + return ( + + {({ css }) => ( + theme.background, + backgroundColor: (theme) => theme.background, + })} + > +

{t("show.nextUp")}

+ +
+ )} +
+ ); +}; + const SerieHeader = ({ children, ...props }: any) => { const { css, theme } = useYoshiki(); const [slug] = useQueryState("slug", undefined!); @@ -95,6 +117,16 @@ const SerieHeader = ({ children, ...props }: any) => { )} >
+ { + const nextEntry = (serie as Serie).nextEntry; + if (!nextEntry) return null; + return ; + }} + Loader={NextUp.Loader} + /> {/* */} {/* */}