mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-31 14:33:50 -04:00
Add next up in series detail page
This commit is contained in:
parent
666e4e2e6b
commit
5ed43c85de
@ -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,
|
||||
|
@ -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(),
|
||||
|
@ -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(),
|
||||
|
@ -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);
|
||||
|
@ -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(),
|
||||
|
@ -12,7 +12,7 @@ export type Layout = {
|
||||
layout: "grid" | "horizontal" | "vertical";
|
||||
};
|
||||
|
||||
export const InfiniteFetch = <Data, Props>({
|
||||
export const InfiniteFetch = <Data,>({
|
||||
query,
|
||||
placeholderCount = 2,
|
||||
incremental = false,
|
||||
@ -34,7 +34,7 @@ export const InfiniteFetch = <Data, Props>({
|
||||
Empty?: JSX.Element;
|
||||
incremental?: boolean;
|
||||
divider?: true | ComponentType;
|
||||
Header?: ComponentType<Props & { children: JSX.Element }> | ReactElement;
|
||||
Header?: ComponentType<{ children: JSX.Element }> | ReactElement;
|
||||
fetchMore?: boolean;
|
||||
}): JSX.Element | null => {
|
||||
const { numColumns, size, gap } = useBreakpointMap(layout);
|
||||
|
@ -9,8 +9,8 @@ export const Fetch = <Data,>({
|
||||
Loader,
|
||||
}: {
|
||||
query: QueryIdentifier<Data>;
|
||||
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");
|
||||
|
@ -826,6 +826,6 @@ Header.query = (
|
||||
parser: Show,
|
||||
path: ["api", `${kind}s`, slug],
|
||||
params: {
|
||||
with: ["studios"],
|
||||
with: ["studios", ...(kind === "serie" ? ["firstEntry", "nextEntry"] : [])],
|
||||
},
|
||||
});
|
||||
|
@ -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<Season> => ({
|
||||
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<ComponentProps<typeof InfiniteFetch>>) => {
|
||||
} & Partial<ComponentProps<typeof InfiniteFetch<Entry>>>) => {
|
||||
const { t } = useTranslation();
|
||||
const { items: seasons, error } = useInfiniteFetch(SeasonHeader.query(slug));
|
||||
|
||||
|
@ -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 (
|
||||
<SwitchVariant>
|
||||
{({ css }) => (
|
||||
@ -60,10 +57,10 @@ export const ShowWatchStatusCard = ({
|
||||
>
|
||||
<H2 {...css({ marginLeft: ts(2) })}>{t("show.nextUp")}</H2>
|
||||
<EntryLine
|
||||
{...nextEpisode}
|
||||
{...nextEntry}
|
||||
serieSlug={null}
|
||||
watchedPercent={watchedPercent || null}
|
||||
displayNumber={entryDisplayNumber(nextEpisode)}
|
||||
watchedPercent={nextEntry.progress.percent}
|
||||
displayNumber={entryDisplayNumber(nextEntry)}
|
||||
onHoverIn={() => setFocus(true)}
|
||||
onHoverOut={() => setFocus(false)}
|
||||
onFocus={() => setFocus(true)}
|
||||
@ -75,6 +72,31 @@ export const ShowWatchStatusCard = ({
|
||||
);
|
||||
};
|
||||
|
||||
NextUp.Loader = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SwitchVariant>
|
||||
{({ css }) => (
|
||||
<Container
|
||||
{...css({
|
||||
marginY: ts(2),
|
||||
borderRadius: 16,
|
||||
overflow: "hidden",
|
||||
borderWidth: ts(0.5),
|
||||
borderStyle: "solid",
|
||||
borderColor: (theme) => theme.background,
|
||||
backgroundColor: (theme) => theme.background,
|
||||
})}
|
||||
>
|
||||
<H2 {...css({ marginLeft: ts(2) })}>{t("show.nextUp")}</H2>
|
||||
<EntryLine.Loader />
|
||||
</Container>
|
||||
)}
|
||||
</SwitchVariant>
|
||||
);
|
||||
};
|
||||
|
||||
const SerieHeader = ({ children, ...props }: any) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
const [slug] = useQueryState("slug", undefined!);
|
||||
@ -95,6 +117,16 @@ const SerieHeader = ({ children, ...props }: any) => {
|
||||
)}
|
||||
>
|
||||
<Header kind="serie" slug={slug} />
|
||||
<Fetch
|
||||
// Use the same fetch query as header
|
||||
query={Header.query("serie", slug)}
|
||||
Render={(serie) => {
|
||||
const nextEntry = (serie as Serie).nextEntry;
|
||||
if (!nextEntry) return null;
|
||||
return <NextUp {...nextEntry} />;
|
||||
}}
|
||||
Loader={NextUp.Loader}
|
||||
/>
|
||||
{/* <DetailsCollections type="serie" slug={slug} /> */}
|
||||
{/* <Staff slug={slug} /> */}
|
||||
<SvgWave
|
||||
|
Loading…
x
Reference in New Issue
Block a user