mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-31 14:33:50 -04:00
Make series page
This commit is contained in:
parent
d5c7ee40bc
commit
42cce837e4
3
front/src/app/(app)/series/[slug].tsx
Normal file
3
front/src/app/(app)/series/[slug].tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { SerieDetails } from "~/ui/details";
|
||||||
|
|
||||||
|
export default SerieDetails;
|
@ -6,7 +6,7 @@ import { Platform, type PressableProps, View } from "react-native";
|
|||||||
import { percent, type Stylable, useYoshiki } from "yoshiki/native";
|
import { percent, type Stylable, useYoshiki } from "yoshiki/native";
|
||||||
import { EntryContext } from "~/components/items/context-menus";
|
import { EntryContext } from "~/components/items/context-menus";
|
||||||
import { ItemProgress } from "~/components/items/item-grid";
|
import { ItemProgress } from "~/components/items/item-grid";
|
||||||
import type { KImage, WatchStatusV } from "~/models";
|
import type { KImage } from "~/models";
|
||||||
import {
|
import {
|
||||||
focusReset,
|
focusReset,
|
||||||
H6,
|
H6,
|
||||||
@ -34,7 +34,6 @@ export const EntryLine = ({
|
|||||||
airDate,
|
airDate,
|
||||||
runtime,
|
runtime,
|
||||||
watchedPercent,
|
watchedPercent,
|
||||||
watchedStatus,
|
|
||||||
href,
|
href,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
@ -48,7 +47,6 @@ export const EntryLine = ({
|
|||||||
airDate: Date | null;
|
airDate: Date | null;
|
||||||
runtime: number | null;
|
runtime: number | null;
|
||||||
watchedPercent: number | null;
|
watchedPercent: number | null;
|
||||||
watchedStatus: WatchStatusV | null;
|
|
||||||
href: string;
|
href: string;
|
||||||
} & PressableProps) => {
|
} & PressableProps) => {
|
||||||
const [moreOpened, setMoreOpened] = useState(false);
|
const [moreOpened, setMoreOpened] = useState(false);
|
||||||
@ -92,7 +90,7 @@ export const EntryLine = ({
|
|||||||
}}
|
}}
|
||||||
{...(css({ flexShrink: 0, m: ts(1), borderRadius: 6 }) as any)}
|
{...(css({ flexShrink: 0, m: ts(1), borderRadius: 6 }) as any)}
|
||||||
>
|
>
|
||||||
{(watchedPercent || watchedStatus === "completed") && (
|
{watchedPercent && (
|
||||||
<ItemProgress watchPercent={watchedPercent ?? 100} />
|
<ItemProgress watchPercent={watchedPercent ?? 100} />
|
||||||
)}
|
)}
|
||||||
</ImageBackground>
|
</ImageBackground>
|
||||||
@ -124,7 +122,6 @@ export const EntryLine = ({
|
|||||||
<EntryContext
|
<EntryContext
|
||||||
slug={slug}
|
slug={slug}
|
||||||
serieSlug={serieSlug}
|
serieSlug={serieSlug}
|
||||||
status={watchedStatus}
|
|
||||||
isOpen={moreOpened}
|
isOpen={moreOpened}
|
||||||
setOpen={(v) => setMoreOpened(v)}
|
setOpen={(v) => setMoreOpened(v)}
|
||||||
{...css([
|
{...css([
|
@ -1,16 +1,17 @@
|
|||||||
export * from "./entry-box";
|
import type { Entry } from "~/models";
|
||||||
export * from "./entry-list";
|
|
||||||
|
|
||||||
export const episodeDisplayNumber = (episode: {
|
export * from "./entry-box";
|
||||||
seasonNumber?: number | null;
|
export * from "./entry-line";
|
||||||
episodeNumber?: number | null;
|
|
||||||
absoluteNumber?: number | null;
|
export const entryDisplayNumber = (entry: Entry) => {
|
||||||
}) => {
|
switch (entry.kind) {
|
||||||
if (
|
case "episode":
|
||||||
typeof episode.seasonNumber === "number" &&
|
return `S${entry.seasonNumber}:E${entry.episodeNumber}`;
|
||||||
typeof episode.episodeNumber === "number"
|
case "special":
|
||||||
)
|
return `SP${entry.number}`
|
||||||
return `S${episode.seasonNumber}:E${episode.episodeNumber}`;
|
case "movie":
|
||||||
if (episode.absoluteNumber) return episode.absoluteNumber.toString();
|
return "";
|
||||||
return "??";
|
default:
|
||||||
|
return "??";
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -15,14 +15,54 @@ import { watchListIcon } from "./watchlist-info";
|
|||||||
// import { useDownloader } from "../../packages/ui/src/downloadses/ui/src/downloads";
|
// import { useDownloader } from "../../packages/ui/src/downloadses/ui/src/downloads";
|
||||||
|
|
||||||
export const EntryContext = ({
|
export const EntryContext = ({
|
||||||
kind = "entry",
|
|
||||||
slug,
|
slug,
|
||||||
serieSlug,
|
serieSlug,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
serieSlug: string | null;
|
||||||
|
slug: string;
|
||||||
|
} & Partial<ComponentProps<typeof Menu<typeof IconButton>>>) => {
|
||||||
|
// const downloader = useDownloader();
|
||||||
|
const { css } = useYoshiki();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Menu
|
||||||
|
Trigger={IconButton}
|
||||||
|
icon={MoreVert}
|
||||||
|
{...tooltip(t("misc.more"))}
|
||||||
|
{...(css([Platform.OS !== "web" && { display: "none" }], props) as any)}
|
||||||
|
>
|
||||||
|
{serieSlug && (
|
||||||
|
<Menu.Item
|
||||||
|
label={t("home.episodeMore.goToShow")}
|
||||||
|
icon={Info}
|
||||||
|
href={`/series/${serieSlug}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* <Menu.Item */}
|
||||||
|
{/* label={t("home.episodeMore.download")} */}
|
||||||
|
{/* icon={Download} */}
|
||||||
|
{/* onSelect={() => downloader(type, slug)} */}
|
||||||
|
{/* /> */}
|
||||||
|
<Menu.Item
|
||||||
|
label={t("home.episodeMore.mediainfo")}
|
||||||
|
icon={MovieInfo}
|
||||||
|
href={`/entries/${slug}/info`}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ItemContext = ({
|
||||||
|
kind,
|
||||||
|
slug,
|
||||||
status,
|
status,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
kind?: "serie" | "movie" | "entry";
|
kind: "movie" | "serie";
|
||||||
serieSlug?: string | null;
|
|
||||||
slug: string;
|
slug: string;
|
||||||
status: WatchStatusV | null;
|
status: WatchStatusV | null;
|
||||||
} & Partial<ComponentProps<typeof Menu<typeof IconButton>>>) => {
|
} & Partial<ComponentProps<typeof Menu<typeof IconButton>>>) => {
|
||||||
@ -32,10 +72,7 @@ export const EntryContext = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
path:
|
path: [kind, slug, "watchStatus"],
|
||||||
kind === "entry"
|
|
||||||
? ["serie", serieSlug!, "entries", slug]
|
|
||||||
: [kind, slug, "watchStatus"],
|
|
||||||
compute: (newStatus: WatchStatusV | null) => ({
|
compute: (newStatus: WatchStatusV | null) => ({
|
||||||
method: newStatus ? "POST" : "DELETE",
|
method: newStatus ? "POST" : "DELETE",
|
||||||
params: newStatus ? { status: newStatus } : undefined,
|
params: newStatus ? { status: newStatus } : undefined,
|
||||||
@ -55,18 +92,8 @@ export const EntryContext = ({
|
|||||||
Trigger={IconButton}
|
Trigger={IconButton}
|
||||||
icon={MoreVert}
|
icon={MoreVert}
|
||||||
{...tooltip(t("misc.more"))}
|
{...tooltip(t("misc.more"))}
|
||||||
{...(css(
|
{...(css([Platform.OS !== "web" && { display: "none" }], props) as any)}
|
||||||
[Platform.OS !== "web" && { display: "none" }],
|
|
||||||
props,
|
|
||||||
) as any)}
|
|
||||||
>
|
>
|
||||||
{serieSlug && (
|
|
||||||
<Menu.Item
|
|
||||||
label={t("home.episodeMore.goToShow")}
|
|
||||||
icon={Info}
|
|
||||||
href={`/serie/${serieSlug}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Menu.Sub
|
<Menu.Sub
|
||||||
label={account ? t("show.watchlistEdit") : t("show.watchlistLogin")}
|
label={account ? t("show.watchlistEdit") : t("show.watchlistLogin")}
|
||||||
disabled={!account}
|
disabled={!account}
|
||||||
@ -89,7 +116,7 @@ export const EntryContext = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Menu.Sub>
|
</Menu.Sub>
|
||||||
{kind !== "serie" && (
|
{kind === "movie" && (
|
||||||
<>
|
<>
|
||||||
{/* <Menu.Item */}
|
{/* <Menu.Item */}
|
||||||
{/* label={t("home.episodeMore.download")} */}
|
{/* label={t("home.episodeMore.download")} */}
|
||||||
@ -99,7 +126,7 @@ export const EntryContext = ({
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
label={t("home.episodeMore.mediainfo")}
|
label={t("home.episodeMore.mediainfo")}
|
||||||
icon={MovieInfo}
|
icon={MovieInfo}
|
||||||
href={`/${kind}/${slug}/info`}
|
href={`/movies/${slug}/info`}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -117,24 +144,3 @@ export const EntryContext = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ItemContext = ({
|
|
||||||
kind,
|
|
||||||
slug,
|
|
||||||
status,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
kind: "movie" | "serie";
|
|
||||||
slug: string;
|
|
||||||
status: WatchStatusV | null;
|
|
||||||
} & Partial<ComponentProps<typeof Menu<typeof IconButton>>>) => {
|
|
||||||
return (
|
|
||||||
<EntryContext
|
|
||||||
kind={kind}
|
|
||||||
slug={slug}
|
|
||||||
status={status}
|
|
||||||
serieSlug={null}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -74,9 +74,11 @@ export const Special = Base.extend({
|
|||||||
});
|
});
|
||||||
export type Special = z.infer<typeof Special>;
|
export type Special = z.infer<typeof Special>;
|
||||||
|
|
||||||
export const Entry = z.discriminatedUnion("kind", [
|
export const Entry = z
|
||||||
Episode,
|
.discriminatedUnion("kind", [Episode, MovieEntry, Special])
|
||||||
MovieEntry,
|
.transform((x) => ({
|
||||||
Special,
|
...x,
|
||||||
]);
|
// TODO: don't just pick the first video, be smart about it
|
||||||
|
href: x.videos.length ? `/watch/${x.videos[0].slug}` : null,
|
||||||
|
}));
|
||||||
export type Entry = z.infer<typeof Entry>;
|
export type Entry = z.infer<typeof Entry>;
|
||||||
|
@ -2,6 +2,5 @@ export * from "./breakpoint";
|
|||||||
export * from "./capitalize";
|
export * from "./capitalize";
|
||||||
export * from "./head";
|
export * from "./head";
|
||||||
export * from "./nojs";
|
export * from "./nojs";
|
||||||
export * from "./page-style";
|
|
||||||
export * from "./spacing";
|
export * from "./spacing";
|
||||||
export * from "./touchonly";
|
export * from "./touchonly";
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
||||||
|
|
||||||
export const usePageStyle = () => {
|
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
return { paddingBottom: insets.bottom } as const;
|
|
||||||
};
|
|
@ -1,3 +0,0 @@
|
|||||||
export const usePageStyle = () => {
|
|
||||||
return {} as const;
|
|
||||||
};
|
|
@ -12,7 +12,7 @@ export type Layout = {
|
|||||||
layout: "grid" | "horizontal" | "vertical";
|
layout: "grid" | "horizontal" | "vertical";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InfiniteFetch = <Data, Props, _, Kind extends number | string>({
|
export const InfiniteFetch = <Data, Props>({
|
||||||
query,
|
query,
|
||||||
placeholderCount = 2,
|
placeholderCount = 2,
|
||||||
incremental = false,
|
incremental = false,
|
||||||
@ -22,11 +22,7 @@ export const InfiniteFetch = <Data, Props, _, Kind extends number | string>({
|
|||||||
Empty,
|
Empty,
|
||||||
divider,
|
divider,
|
||||||
Header,
|
Header,
|
||||||
headerProps,
|
|
||||||
getItemType,
|
|
||||||
getItemSize,
|
|
||||||
fetchMore = true,
|
fetchMore = true,
|
||||||
nested = false,
|
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
query: QueryIdentifier<Data>;
|
query: QueryIdentifier<Data>;
|
||||||
@ -39,11 +35,7 @@ export const InfiniteFetch = <Data, Props, _, Kind extends number | string>({
|
|||||||
incremental?: boolean;
|
incremental?: boolean;
|
||||||
divider?: true | ComponentType;
|
divider?: true | ComponentType;
|
||||||
Header?: ComponentType<Props & { children: JSX.Element }> | ReactElement;
|
Header?: ComponentType<Props & { children: JSX.Element }> | ReactElement;
|
||||||
headerProps?: Props;
|
|
||||||
getItemType?: (item: Data | null, index: number) => Kind;
|
|
||||||
getItemSize?: (kind: Kind) => number;
|
|
||||||
fetchMore?: boolean;
|
fetchMore?: boolean;
|
||||||
nested?: boolean;
|
|
||||||
}): JSX.Element | null => {
|
}): JSX.Element | null => {
|
||||||
const { numColumns, size, gap } = useBreakpointMap(layout);
|
const { numColumns, size, gap } = useBreakpointMap(layout);
|
||||||
const [setOffline, clearOffline] = useSetError("offline");
|
const [setOffline, clearOffline] = useSetError("offline");
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import MenuIcon from "@material-symbols/svg-400/rounded/menu-fill.svg";
|
import MenuIcon from "@material-symbols/svg-400/rounded/menu-fill.svg";
|
||||||
import type { ComponentType } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { rem, useYoshiki } from "yoshiki/native";
|
import { rem, useYoshiki } from "yoshiki/native";
|
||||||
import { type Episode, type Season, useInfiniteFetch } from "~/models";
|
import { EntryLine, entryDisplayNumber } from "~/components/entries";
|
||||||
|
import { Entry, Season } from "~/models";
|
||||||
import {
|
import {
|
||||||
H2,
|
H2,
|
||||||
HR,
|
HR,
|
||||||
@ -13,22 +14,21 @@ import {
|
|||||||
Skeleton,
|
Skeleton,
|
||||||
tooltip,
|
tooltip,
|
||||||
ts,
|
ts,
|
||||||
usePageStyle,
|
|
||||||
} from "~/primitives";
|
} from "~/primitives";
|
||||||
import type { QueryIdentifier } from "~/query";
|
import { type QueryIdentifier, useInfiniteFetch } from "~/query";
|
||||||
import { InfiniteFetch } from "~/query/fetch-infinite";
|
import { InfiniteFetch } from "~/query/fetch-infinite";
|
||||||
import { EpisodeLine, episodeDisplayNumber } from "./episode";
|
import { EmptyView } from "~/ui/errors";
|
||||||
|
|
||||||
type SeasonProcessed = Season & { href: string };
|
|
||||||
|
|
||||||
export const SeasonHeader = ({
|
export const SeasonHeader = ({
|
||||||
|
serieSlug,
|
||||||
seasonNumber,
|
seasonNumber,
|
||||||
name,
|
name,
|
||||||
seasons,
|
seasons,
|
||||||
}: {
|
}: {
|
||||||
|
serieSlug: string;
|
||||||
seasonNumber: number;
|
seasonNumber: number;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
seasons?: SeasonProcessed[];
|
seasons: Season[];
|
||||||
}) => {
|
}) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -62,17 +62,15 @@ export const SeasonHeader = ({
|
|||||||
icon={MenuIcon}
|
icon={MenuIcon}
|
||||||
{...tooltip(t("show.jumpToSeason"))}
|
{...tooltip(t("show.jumpToSeason"))}
|
||||||
>
|
>
|
||||||
{seasons
|
{seasons.map((x) => (
|
||||||
?.filter((x) => x.episodesCount > 0)
|
<Menu.Item
|
||||||
.map((x) => (
|
key={x.seasonNumber}
|
||||||
<Menu.Item
|
label={`${x.seasonNumber}: ${
|
||||||
key={x.seasonNumber}
|
x.name ?? t("show.season", { number: x.seasonNumber })
|
||||||
label={`${x.seasonNumber}: ${
|
} (${x.entryCount})`}
|
||||||
x.name ?? t("show.season", { number: x.seasonNumber })
|
href={`/series/${serieSlug}?season=${x.seasonNumber}`}
|
||||||
} (${x.episodesCount})`}
|
/>
|
||||||
href={x.href}
|
))}
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Menu>
|
</Menu>
|
||||||
</View>
|
</View>
|
||||||
<HR />
|
<HR />
|
||||||
@ -115,33 +113,22 @@ SeasonHeader.Loader = () => {
|
|||||||
|
|
||||||
SeasonHeader.query = (slug: string): QueryIdentifier<Season> => ({
|
SeasonHeader.query = (slug: string): QueryIdentifier<Season> => ({
|
||||||
parser: Season,
|
parser: Season,
|
||||||
path: ["series", slug, "seasons"],
|
path: ["api", "series", slug, "seasons"],
|
||||||
params: {
|
params: {
|
||||||
// Fetch all seasons at one, there won't be hundred of thems anyways.
|
// Fetch all seasons at one, there won't be hundred of them anyways.
|
||||||
limit: 0,
|
limit: 0,
|
||||||
},
|
},
|
||||||
infinite: {
|
infinite: true,
|
||||||
value: true,
|
|
||||||
map: (seasons) =>
|
|
||||||
seasons.map((x) => ({
|
|
||||||
...x,
|
|
||||||
href: `/show/${slug}?season=${x.seasonNumber}`,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const EpisodeList = <Props,>({
|
export const EntryList = ({
|
||||||
slug,
|
slug,
|
||||||
season,
|
season,
|
||||||
Header,
|
...props
|
||||||
headerProps,
|
|
||||||
}: {
|
}: {
|
||||||
slug: string;
|
slug: string;
|
||||||
season: string | number;
|
season: string | number;
|
||||||
Header: ComponentType<Props & { children: JSX.Element }>;
|
} & Partial<ComponentProps<typeof InfiniteFetch>>) => {
|
||||||
headerProps: Props;
|
|
||||||
}) => {
|
|
||||||
const pageStyle = usePageStyle();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { items: seasons, error } = useInfiniteFetch(SeasonHeader.query(slug));
|
const { items: seasons, error } = useInfiniteFetch(SeasonHeader.query(slug));
|
||||||
|
|
||||||
@ -149,40 +136,35 @@ export const EpisodeList = <Props,>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<InfiniteFetch
|
<InfiniteFetch
|
||||||
query={EpisodeList.query(slug, season)}
|
query={EntryList.query(slug, season)}
|
||||||
layout={EpisodeLine.layout}
|
layout={EntryLine.layout}
|
||||||
empty={t("show.episode-none")}
|
Empty={<EmptyView message={t("show.episode-none")} />}
|
||||||
divider
|
divider
|
||||||
Header={Header}
|
// getItemType={(item) =>
|
||||||
headerProps={headerProps}
|
// item.kind === "episode" && item.episodeNumber === 1? "withHeader" : "normal"
|
||||||
getItemType={(item) =>
|
// }
|
||||||
!item || item.firstOfSeason ? "withHeader" : "normal"
|
|
||||||
}
|
|
||||||
contentContainerStyle={pageStyle}
|
|
||||||
placeholderCount={5}
|
placeholderCount={5}
|
||||||
Render={({ item }) => {
|
Render={({ item }) => {
|
||||||
const sea = item?.firstOfSeason
|
const sea =
|
||||||
? seasons?.find((x) => x.seasonNumber === item.seasonNumber)
|
item.kind === "episode" && item.episodeNumber === 1
|
||||||
: null;
|
? seasons?.find((x) => x.seasonNumber === item.seasonNumber)
|
||||||
|
: null;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{item.firstOfSeason &&
|
{sea && (
|
||||||
(sea ? (
|
<SeasonHeader
|
||||||
<SeasonHeader
|
serieSlug={slug}
|
||||||
name={sea.name}
|
name={sea.name}
|
||||||
seasonNumber={sea.seasonNumber}
|
seasonNumber={sea.seasonNumber}
|
||||||
seasons={seasons}
|
seasons={seasons ?? []}
|
||||||
/>
|
/>
|
||||||
) : (
|
)}
|
||||||
<SeasonHeader.Loader />
|
<EntryLine
|
||||||
))}
|
|
||||||
<EpisodeLine
|
|
||||||
{...item}
|
{...item}
|
||||||
// Don't display "Go to show"
|
// Don't display "Go to serie"
|
||||||
showSlug={null}
|
serieSlug={null}
|
||||||
displayNumber={episodeDisplayNumber(item)}
|
displayNumber={entryDisplayNumber(item)}
|
||||||
watchedPercent={item.watchStatus?.watchedPercent ?? null}
|
watchedPercent={item.progress.percent}
|
||||||
watchedStatus={item.watchStatus?.status ?? null}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -190,32 +172,22 @@ export const EpisodeList = <Props,>({
|
|||||||
Loader={({ index }) => (
|
Loader={({ index }) => (
|
||||||
<>
|
<>
|
||||||
{index === 0 && <SeasonHeader.Loader />}
|
{index === 0 && <SeasonHeader.Loader />}
|
||||||
<EpisodeLine.Loader />
|
<EntryLine.Loader />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
EpisodeList.query = (
|
EntryList.query = (
|
||||||
slug: string,
|
slug: string,
|
||||||
season: string | number,
|
season: string | number,
|
||||||
): QueryIdentifier<Episode, Episode & { firstOfSeason?: boolean }> => ({
|
): QueryIdentifier<Entry> => ({
|
||||||
parser: EpisodeP,
|
parser: Entry,
|
||||||
path: ["show", slug, "episode"],
|
path: ["api", "series", slug, "entries"],
|
||||||
params: {
|
params: {
|
||||||
filter: season ? `seasonNumber gte ${season}` : undefined,
|
filter: season ? `seasonNumber gte ${season}` : undefined,
|
||||||
fields: ["watchStatus"],
|
|
||||||
},
|
|
||||||
infinite: {
|
|
||||||
value: true,
|
|
||||||
map: (episodes) => {
|
|
||||||
let currentSeason: number | null = null;
|
|
||||||
return episodes.map((x) => {
|
|
||||||
if (x.seasonNumber === currentSeason) return x;
|
|
||||||
currentSeason = x.seasonNumber;
|
|
||||||
return { ...x, firstOfSeason: true };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
infinite: true,
|
||||||
});
|
});
|
||||||
|
@ -3,11 +3,11 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Platform, View } from "react-native";
|
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 { Container, focusReset, H2, SwitchVariant, ts } from "~/primitives";
|
import { Container, focusReset, H2, SwitchVariant, ts } from "~/primitives";
|
||||||
import { useQueryState } from "~/utils";
|
import { useQueryState } from "~/utils";
|
||||||
import { EpisodeLine, episodeDisplayNumber } from "./episode";
|
|
||||||
import { Header } from "./header";
|
import { Header } from "./header";
|
||||||
import { EpisodeList } from "./season";
|
import { EntryList } from "./season";
|
||||||
|
|
||||||
export const SvgWave = (props: SvgProps) => {
|
export const SvgWave = (props: SvgProps) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
@ -31,7 +31,6 @@ export const SvgWave = (props: SvgProps) => {
|
|||||||
|
|
||||||
export const ShowWatchStatusCard = ({
|
export const ShowWatchStatusCard = ({
|
||||||
watchedPercent,
|
watchedPercent,
|
||||||
status,
|
|
||||||
nextEpisode,
|
nextEpisode,
|
||||||
}: ShowWatchStatus) => {
|
}: ShowWatchStatus) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -60,12 +59,11 @@ export const ShowWatchStatusCard = ({
|
|||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
<H2 {...css({ marginLeft: ts(2) })}>{t("show.nextUp")}</H2>
|
<H2 {...css({ marginLeft: ts(2) })}>{t("show.nextUp")}</H2>
|
||||||
<EpisodeLine
|
<EntryLine
|
||||||
{...nextEpisode}
|
{...nextEpisode}
|
||||||
showSlug={null}
|
serieSlug={null}
|
||||||
watchedPercent={watchedPercent || null}
|
watchedPercent={watchedPercent || null}
|
||||||
watchedStatus={status || null}
|
displayNumber={entryDisplayNumber(nextEpisode)}
|
||||||
displayNumber={episodeDisplayNumber(nextEpisode)}
|
|
||||||
onHoverIn={() => setFocus(true)}
|
onHoverIn={() => setFocus(true)}
|
||||||
onHoverOut={() => setFocus(false)}
|
onHoverOut={() => setFocus(false)}
|
||||||
onFocus={() => setFocus(true)}
|
onFocus={() => setFocus(true)}
|
||||||
@ -77,8 +75,9 @@ export const ShowWatchStatusCard = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ShowHeader = ({ children, slug, ...props }: any) => {
|
const SerieHeader = ({ children, ...props }: any) => {
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
|
const [slug] = useQueryState("slug", undefined!);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@ -109,18 +108,14 @@ const ShowHeader = ({ children, slug, ...props }: any) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ShowDetails = () => {
|
export const SerieDetails = () => {
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
const [slug] = useQueryState("slug", undefined!);
|
const [slug] = useQueryState("slug", undefined!);
|
||||||
|
const [season] = useQueryState("season", undefined!);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...css({ bg: theme.variant.background, flex: 1 })}>
|
<View {...css({ bg: theme.variant.background, flex: 1 })}>
|
||||||
<EpisodeList
|
<EntryList slug={slug} season={season} Header={SerieHeader} />
|
||||||
slug={slug}
|
|
||||||
season={season}
|
|
||||||
Header={ShowHeader}
|
|
||||||
headerProps={{ slug }}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user