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([
|
export const BaseEpisode = t.Composite([
|
||||||
t.Object({
|
t.Object({
|
||||||
kind: t.Literal("episode"),
|
kind: t.Literal("episode"),
|
||||||
order: t.Number({ minimum: 1, description: "Absolute playback order." }),
|
order: t.Number({ description: "Absolute playback order." }),
|
||||||
seasonNumber: t.Integer(),
|
seasonNumber: t.Integer(),
|
||||||
episodeNumber: t.Integer(),
|
episodeNumber: t.Integer(),
|
||||||
externalId: EpisodeId,
|
externalId: EpisodeId,
|
||||||
|
@ -18,7 +18,6 @@ export const BaseMovieEntry = t.Composite(
|
|||||||
t.Object({
|
t.Object({
|
||||||
kind: t.Literal("movie"),
|
kind: t.Literal("movie"),
|
||||||
order: t.Number({
|
order: t.Number({
|
||||||
minimum: 1,
|
|
||||||
description: "Absolute playback order. Can be mixed with episodes.",
|
description: "Absolute playback order. Can be mixed with episodes.",
|
||||||
}),
|
}),
|
||||||
externalId: ExternalId(),
|
externalId: ExternalId(),
|
||||||
|
@ -17,10 +17,9 @@ export const BaseSpecial = t.Composite(
|
|||||||
t.Object({
|
t.Object({
|
||||||
kind: t.Literal("special"),
|
kind: t.Literal("special"),
|
||||||
order: t.Number({
|
order: t.Number({
|
||||||
minimum: 1,
|
|
||||||
description: "Absolute playback order. Can be mixed with episodes.",
|
description: "Absolute playback order. Can be mixed with episodes.",
|
||||||
}),
|
}),
|
||||||
number: t.Integer({ minimum: 1 }),
|
number: t.Integer(),
|
||||||
externalId: EpisodeId,
|
externalId: EpisodeId,
|
||||||
}),
|
}),
|
||||||
BaseEntry(),
|
BaseEntry(),
|
||||||
|
@ -47,7 +47,7 @@ export const EntryLine = ({
|
|||||||
airDate: Date | null;
|
airDate: Date | null;
|
||||||
runtime: number | null;
|
runtime: number | null;
|
||||||
watchedPercent: number | null;
|
watchedPercent: number | null;
|
||||||
href: string;
|
href: string | null;
|
||||||
} & PressableProps) => {
|
} & PressableProps) => {
|
||||||
const [moreOpened, setMoreOpened] = useState(false);
|
const [moreOpened, setMoreOpened] = useState(false);
|
||||||
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
||||||
|
@ -8,7 +8,7 @@ export const Season = z.object({
|
|||||||
seasonNumber: z.int().gte(0),
|
seasonNumber: z.int().gte(0),
|
||||||
name: z.string().nullable(),
|
name: z.string().nullable(),
|
||||||
description: z.string().nullable(),
|
description: z.string().nullable(),
|
||||||
entryCount: z.int().gte(0),
|
entriesCount: z.int().gte(0),
|
||||||
availableCount: z.int().gte(0),
|
availableCount: z.int().gte(0),
|
||||||
startAir: zdate().nullable(),
|
startAir: zdate().nullable(),
|
||||||
endAir: zdate().nullable(),
|
endAir: zdate().nullable(),
|
||||||
|
@ -12,7 +12,7 @@ export type Layout = {
|
|||||||
layout: "grid" | "horizontal" | "vertical";
|
layout: "grid" | "horizontal" | "vertical";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InfiniteFetch = <Data, Props>({
|
export const InfiniteFetch = <Data,>({
|
||||||
query,
|
query,
|
||||||
placeholderCount = 2,
|
placeholderCount = 2,
|
||||||
incremental = false,
|
incremental = false,
|
||||||
@ -34,7 +34,7 @@ export const InfiniteFetch = <Data, Props>({
|
|||||||
Empty?: JSX.Element;
|
Empty?: JSX.Element;
|
||||||
incremental?: boolean;
|
incremental?: boolean;
|
||||||
divider?: true | ComponentType;
|
divider?: true | ComponentType;
|
||||||
Header?: ComponentType<Props & { children: JSX.Element }> | ReactElement;
|
Header?: ComponentType<{ children: JSX.Element }> | ReactElement;
|
||||||
fetchMore?: boolean;
|
fetchMore?: boolean;
|
||||||
}): JSX.Element | null => {
|
}): JSX.Element | null => {
|
||||||
const { numColumns, size, gap } = useBreakpointMap(layout);
|
const { numColumns, size, gap } = useBreakpointMap(layout);
|
||||||
|
@ -9,8 +9,8 @@ export const Fetch = <Data,>({
|
|||||||
Loader,
|
Loader,
|
||||||
}: {
|
}: {
|
||||||
query: QueryIdentifier<Data>;
|
query: QueryIdentifier<Data>;
|
||||||
Render: (item: Data) => ReactElement;
|
Render: (item: Data) => ReactElement | null;
|
||||||
Loader: () => ReactElement;
|
Loader: () => ReactElement | null;
|
||||||
}): JSX.Element | null => {
|
}): JSX.Element | null => {
|
||||||
const { data, isPaused, error } = useFetch(query);
|
const { data, isPaused, error } = useFetch(query);
|
||||||
const [setError] = useSetError("fetch");
|
const [setError] = useSetError("fetch");
|
||||||
|
@ -826,6 +826,6 @@ Header.query = (
|
|||||||
parser: Show,
|
parser: Show,
|
||||||
path: ["api", `${kind}s`, slug],
|
path: ["api", `${kind}s`, slug],
|
||||||
params: {
|
params: {
|
||||||
with: ["studios"],
|
with: ["studios", ...(kind === "serie" ? ["firstEntry", "nextEntry"] : [])],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -67,7 +67,7 @@ export const SeasonHeader = ({
|
|||||||
key={x.seasonNumber}
|
key={x.seasonNumber}
|
||||||
label={`${x.seasonNumber}: ${
|
label={`${x.seasonNumber}: ${
|
||||||
x.name ?? t("show.season", { number: x.seasonNumber })
|
x.name ?? t("show.season", { number: x.seasonNumber })
|
||||||
} (${x.entryCount})`}
|
} (${x.entriesCount})`}
|
||||||
href={`/series/${serieSlug}?season=${x.seasonNumber}`}
|
href={`/series/${serieSlug}?season=${x.seasonNumber}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@ -115,8 +115,8 @@ SeasonHeader.query = (slug: string): QueryIdentifier<Season> => ({
|
|||||||
parser: Season,
|
parser: Season,
|
||||||
path: ["api", "series", slug, "seasons"],
|
path: ["api", "series", slug, "seasons"],
|
||||||
params: {
|
params: {
|
||||||
// Fetch all seasons at one, there won't be hundred of them anyways.
|
// I don't wanna deal with pagination, no serie has more than 100 seasons anyways, right?
|
||||||
limit: 0,
|
limit: 100,
|
||||||
},
|
},
|
||||||
infinite: true,
|
infinite: true,
|
||||||
});
|
});
|
||||||
@ -128,7 +128,7 @@ export const EntryList = ({
|
|||||||
}: {
|
}: {
|
||||||
slug: string;
|
slug: string;
|
||||||
season: string | number;
|
season: string | number;
|
||||||
} & Partial<ComponentProps<typeof InfiniteFetch>>) => {
|
} & Partial<ComponentProps<typeof InfiniteFetch<Entry>>>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { items: seasons, error } = useInfiniteFetch(SeasonHeader.query(slug));
|
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 Svg, { Path, type SvgProps } from "react-native-svg";
|
||||||
import { percent, useYoshiki } from "yoshiki/native";
|
import { percent, useYoshiki } from "yoshiki/native";
|
||||||
import { EntryLine, entryDisplayNumber } from "~/components/entries";
|
import { EntryLine, entryDisplayNumber } from "~/components/entries";
|
||||||
|
import type { Entry, Serie } from "~/models";
|
||||||
import { Container, focusReset, H2, SwitchVariant, ts } from "~/primitives";
|
import { Container, focusReset, H2, SwitchVariant, ts } from "~/primitives";
|
||||||
|
import { Fetch } from "~/query";
|
||||||
import { useQueryState } from "~/utils";
|
import { useQueryState } from "~/utils";
|
||||||
import { Header } from "./header";
|
import { Header } from "./header";
|
||||||
import { EntryList } from "./season";
|
import { EntryList } from "./season";
|
||||||
@ -29,15 +31,10 @@ export const SvgWave = (props: SvgProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ShowWatchStatusCard = ({
|
export const NextUp = (nextEntry: Entry) => {
|
||||||
watchedPercent,
|
|
||||||
nextEpisode,
|
|
||||||
}: ShowWatchStatus) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [focused, setFocus] = useState(false);
|
const [focused, setFocus] = useState(false);
|
||||||
|
|
||||||
if (!nextEpisode) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SwitchVariant>
|
<SwitchVariant>
|
||||||
{({ css }) => (
|
{({ css }) => (
|
||||||
@ -60,10 +57,10 @@ export const ShowWatchStatusCard = ({
|
|||||||
>
|
>
|
||||||
<H2 {...css({ marginLeft: ts(2) })}>{t("show.nextUp")}</H2>
|
<H2 {...css({ marginLeft: ts(2) })}>{t("show.nextUp")}</H2>
|
||||||
<EntryLine
|
<EntryLine
|
||||||
{...nextEpisode}
|
{...nextEntry}
|
||||||
serieSlug={null}
|
serieSlug={null}
|
||||||
watchedPercent={watchedPercent || null}
|
watchedPercent={nextEntry.progress.percent}
|
||||||
displayNumber={entryDisplayNumber(nextEpisode)}
|
displayNumber={entryDisplayNumber(nextEntry)}
|
||||||
onHoverIn={() => setFocus(true)}
|
onHoverIn={() => setFocus(true)}
|
||||||
onHoverOut={() => setFocus(false)}
|
onHoverOut={() => setFocus(false)}
|
||||||
onFocus={() => setFocus(true)}
|
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 SerieHeader = ({ children, ...props }: any) => {
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
const [slug] = useQueryState("slug", undefined!);
|
const [slug] = useQueryState("slug", undefined!);
|
||||||
@ -95,6 +117,16 @@ const SerieHeader = ({ children, ...props }: any) => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Header kind="serie" slug={slug} />
|
<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} /> */}
|
{/* <DetailsCollections type="serie" slug={slug} /> */}
|
||||||
{/* <Staff slug={slug} /> */}
|
{/* <Staff slug={slug} /> */}
|
||||||
<SvgWave
|
<SvgWave
|
||||||
|
Loading…
x
Reference in New Issue
Block a user