Make episode list work on mobile

This commit is contained in:
Zoe Roux 2022-12-16 17:30:33 +09:00
parent eabf5e1faf
commit 2ac4c434f5
10 changed files with 124 additions and 86 deletions

View File

@ -35,7 +35,7 @@ export const Container = <AsProps = ViewProps,>({
{
display: "flex",
paddingHorizontal: px(15),
marginHorizontal: "auto",
alignSelf: "center",
width: {
xs: percent(100),
sm: px(540),

View File

@ -24,9 +24,9 @@ import { alpha } from "./themes";
import { ts } from "./utils";
export const HR = ({
orientation,
orientation = "horizontal",
...props
}: { orientation: "vertical" | "horizontal" } & Stylable) => {
}: { orientation?: "vertical" | "horizontal" } & Stylable) => {
const { css } = useYoshiki();
return (

View File

@ -87,5 +87,5 @@ export const ItemGrid = ({
ItemGrid.layout = {
size: px(150),
numColumns: { xs: 3, md: 5, xl: 7 },
numColumns: { xs: 3, sm: 5, xl: 7 },
} satisfies Layout;

View File

@ -22,7 +22,7 @@ import { H6, Image, Link, P, Skeleton, ts } from "@kyoo/primitives";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { Layout, WithLoading } from "../fetch";
import { percent, rem, Stylable, useYoshiki, vw } from "yoshiki/native";
import { percent, rem, Stylable, useYoshiki } from "yoshiki/native";
export const episodeDisplayNumber = (
episode: {
@ -111,5 +111,5 @@ export const EpisodeLine = ({
};
EpisodeLine.layout = {
numColumns: 1,
size: 100, //vw(18) / (16 / 9) + ts(2),
size: 100,
} satisfies Layout;

View File

@ -40,7 +40,7 @@ import {
} from "@kyoo/primitives";
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { Platform, View } from "react-native";
import {
Theme,
md,
@ -55,8 +55,8 @@ import {
Stylable,
} from "yoshiki/native";
import { Fetch } from "../fetch";
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg"
import Theaters from "@material-symbols/svg-400/rounded/theaters-fill.svg"
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
import Theaters from "@material-symbols/svg-400/rounded/theaters-fill.svg";
const TitleLine = ({
isLoading,
@ -100,7 +100,10 @@ const TitleLine = ({
layout={{
width: { xs: percent(50), md: percent(25) },
}}
{...css({ maxWidth: { xs: px(175), sm: "unset" }, flexShrink: 0 })}
{...css({
maxWidth: { xs: px(175), sm: Platform.OS === "web" ? "unset" : 99999999 },
flexShrink: 0,
})}
/>
<View
{...css({

View File

@ -20,23 +20,32 @@
import { Episode, EpisodeP, QueryIdentifier, Season } from "@kyoo/models";
import { Container, SwitchVariant, ts } from "@kyoo/primitives";
import Svg, { SvgProps, Path } from "react-native-svg";
import { Stylable } from "yoshiki/native";
import { View } from "react-native";
import { InfiniteFetch } from "../fetch-infinite";
import { episodeDisplayNumber, EpisodeLine } from "./episode";
import { useTranslation } from "react-i18next";
import { ComponentType } from "react";
const EpisodeGrid = ({ slug, season }: { slug: string; season: string | number }) => {
export const EpisodeList = ({
slug,
season,
Header,
}: {
slug: string;
season: string | number;
Header: ComponentType<{ children: JSX.Element }>;
}) => {
const { t } = useTranslation();
return (
<InfiniteFetch
query={EpisodeGrid.query(slug, season)}
query={EpisodeList.query(slug, season)}
placeholderCount={15}
layout={EpisodeLine.layout}
empty={t("show.episode-none")}
divider
Header={Header}
>
{(item) => (
<EpisodeLine
@ -48,7 +57,7 @@ const EpisodeGrid = ({ slug, season }: { slug: string; season: string | number }
);
};
EpisodeGrid.query = (slug: string, season: string | number): QueryIdentifier<Episode> => ({
EpisodeList.query = (slug: string, season: string | number): QueryIdentifier<Episode> => ({
parser: EpisodeP,
path: ["shows", slug, "episode"],
params: {
@ -57,47 +66,34 @@ EpisodeGrid.query = (slug: string, season: string | number): QueryIdentifier<Epi
infinite: true,
});
const SvgWave = (props: SvgProps) => (
<Svg viewBox="0 372.979 612 52.771" {...props}>
<Path d="M0 375.175c68-5.1 136-.85 204 7.948 68 9.052 136 22.652 204 24.777s136-8.075 170-12.878l34-4.973v35.7H0" />
</Svg>
);
export const SeasonTab = ({
slug,
season,
...props
}: { slug: string; season: number | string } & Stylable) => {
// TODO: handle absolute number only shows (without seasons)
return null;
return (
<SwitchVariant>
{({ css, theme }) => (
<View>
<SvgWave fill={theme.background} {...css({ marginTop: { xs: ts(2), md: 0 } })} />
<View {...css({ bg: (theme) => theme.background }, props)}>
<Container>
{/* <Tabs value={season} onChange={(_, i) => setSeason(i)} aria-label="List of seasons"> */}
{/* {seasons */}
{/* ? seasons.map((x) => ( */}
{/* <Tab */}
{/* key={x.seasonNumber} */}
{/* label={x.name} */}
{/* value={x.seasonNumber} */}
{/* component={Link} */}
{/* to={{ query: { ...router.query, season: x.seasonNumber } }} */}
{/* shallow */}
{/* replace */}
{/* /> */}
{/* )) */}
{/* : [...Array(3)].map((_, i) => ( */}
{/* <Tab key={i} label={<Skeleton width="5rem" />} value={i + 1} disabled /> */}
{/* ))} */}
{/* </Tabs> */}
<EpisodeGrid slug={slug} season={season} />
</Container>
</View>
</View>
)}
</SwitchVariant>
<View>
<Container>
{/* <Tabs value={season} onChange={(_, i) => setSeason(i)} aria-label="List of seasons"> */}
{/* {seasons */}
{/* ? seasons.map((x) => ( */}
{/* <Tab */}
{/* key={x.seasonNumber} */}
{/* label={x.name} */}
{/* value={x.seasonNumber} */}
{/* component={Link} */}
{/* to={{ query: { ...router.query, season: x.seasonNumber } }} */}
{/* shallow */}
{/* replace */}
{/* /> */}
{/* )) */}
{/* : [...Array(3)].map((_, i) => ( */}
{/* <Tab key={i} label={<Skeleton width="5rem" />} value={i + 1} disabled /> */}
{/* ))} */}
{/* </Tabs> */}
</Container>
</View>
);
};

View File

@ -19,11 +19,27 @@
*/
import { QueryIdentifier, QueryPage, Show, ShowP } from "@kyoo/models";
import { Platform, ScrollView } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { Platform, ScrollView, View, ViewProps } from "react-native";
import { percent, useYoshiki, vh } from "yoshiki/native";
import { TransparentLayout } from "../layout";
import { SeasonTab } from "./season";
import { EpisodeList, SeasonTab } from "./season";
import { Header } from "./header";
import Svg, { Path, SvgProps } from "react-native-svg";
import { Container, SwitchVariant } from "@kyoo/primitives";
const SvgWave = (props: SvgProps) => {
const { css } = useYoshiki();
const width = 612;
const height = 52.771;
return (
<View {...css({ width: percent(100), aspectRatio: width / height })}>
<Svg width="100%" height="100%" viewBox="0 372.979 612 52.771" fill="black" {...props}>
<Path d="M0,375.175c68,-5.1,136,-0.85,204,7.948c68,9.052,136,22.652,204,24.777s136,-8.075,170,-12.878l34,-4.973v35.7h-612" />
</Svg>
</View>
);
};
const query = (slug: string): QueryIdentifier<Show> => ({
parser: ShowP,
@ -34,14 +50,39 @@ const query = (slug: string): QueryIdentifier<Show> => ({
});
export const ShowDetails: QueryPage<{ slug: string; season: string }> = ({ slug, season }) => {
const { css } = useYoshiki();
const { css, theme } = useYoshiki();
return (
<ScrollView {...css(Platform.OS === "web" && { overflow: "overlay" as any })}>
const ShowHeader = ({ children, ...props }: ViewProps) => (
<View
{...css(
[
{ bg: (theme) => theme.background },
Platform.OS === "web" && { flexGrow: 1, flexShrink: 1, overflow: "overlay" as any },
],
props,
)}
>
<Header slug={slug} query={query(slug)} />
{/* <Staff slug={slug} /> */}
<SeasonTab slug={slug} season={season} />
</ScrollView>
<SvgWave
fill={theme.variant.background}
{...css({ flexShrink: 0, flexGrow: 1, display: "flex" })}
/>
{/* <SeasonTab slug={slug} season={season} /> */}
<View {...css({ bg: theme.variant.background })}>
<Container>{children}</Container>
</View>
</View>
);
return (
<SwitchVariant>
{({ css, theme }) => (
<View {...css({ bg: theme.background, flex: 1 })}>
<EpisodeList slug={slug} season={season} Header={ShowHeader} />
</View>
)}
</SwitchVariant>
);
};

View File

@ -21,7 +21,7 @@
import { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models";
import { useBreakpointMap, HR } from "@kyoo/primitives";
import { FlashList } from "@shopify/flash-list";
import { ReactElement } from "react";
import { ComponentType, ReactElement } from "react";
import { EmptyView, ErrorView, Layout, WithLoading } from "./fetch";
export const InfiniteFetch = <Data,>({
@ -32,6 +32,7 @@ export const InfiniteFetch = <Data,>({
layout,
empty,
divider = false,
Header,
...props
}: {
query: QueryIdentifier<Data>;
@ -43,7 +44,8 @@ export const InfiniteFetch = <Data,>({
i: number,
) => ReactElement | null;
empty?: string | JSX.Element;
divider?: boolean | JSX.Element;
divider?: boolean | ComponentType;
Header?: ComponentType<{ children: JSX.Element }>;
}): JSX.Element | null => {
if (!query.infinite) console.warn("A non infinite query was passed to an InfiniteFetch.");
@ -57,26 +59,14 @@ export const InfiniteFetch = <Data,>({
return <EmptyView message={empty} />;
}
const placeholders = [
...Array(items ? numColumns - (items.length % numColumns) + numColumns : placeholderCount),
].map((_, i) => ({ id: `gen${i}`, isLoading: true } as Data));
return (
<FlashList
renderItem={({ item, index }) => (
<>
{(divider === true && index !== 0) ? <HR orientation={horizontal ? "vertical" : "horizontal"} /> : divider}
{children({ isLoading: false, ...item } as any, index)}
</>
)}
data={
hasNextPage
? [
...(items || []),
...[
...Array(
items ? numColumns - (items.length % numColumns) + numColumns : placeholderCount,
),
].map((_, i) => ({ id: `gen${i}`, isLoading: true } as Data)),
]
: items
}
renderItem={({ item, index }) => children({ isLoading: false, ...item } as any, index)}
data={hasNextPage !== false ? [...(items || []), ...placeholders] : items}
horizontal={horizontal}
keyExtractor={(item: any) => item.id?.toString()}
numColumns={numColumns}
@ -85,6 +75,8 @@ export const InfiniteFetch = <Data,>({
onEndReachedThreshold={0.5}
onRefresh={refetch}
refreshing={isRefetching}
ItemSeparatorComponent={divider === true ? HR : divider || null}
ListHeaderComponent={Header}
{...props}
/>
);

View File

@ -20,7 +20,7 @@
import { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models";
import { HR } from "@kyoo/primitives";
import { Fragment, ReactElement, useRef } from "react";
import { ComponentType, Fragment, ReactElement, useRef } from "react";
import { Stylable, useYoshiki } from "yoshiki";
import { EmptyView, ErrorView, Layout, WithLoading } from "./fetch";
@ -93,7 +93,8 @@ export const InfiniteFetch = <Data,>({
layout,
horizontal = false,
empty,
divider = false,
divider: Divider = false,
Header,
...props
}: {
query: QueryIdentifier<Data>;
@ -105,7 +106,8 @@ export const InfiniteFetch = <Data,>({
i: number,
) => ReactElement | null;
empty?: string | JSX.Element;
divider?: boolean | JSX.Element;
divider?: boolean | ComponentType;
Header?: ComponentType<{ children: JSX.Element }>;
}): JSX.Element | null => {
if (!query.infinite) console.warn("A non infinite query was passed to an InfiniteFetch.");
@ -118,7 +120,7 @@ export const InfiniteFetch = <Data,>({
return <EmptyView message={empty} />;
}
return (
const list = (
<InfiniteScroll
layout={grid ? "grid" : horizontal ? "horizontal" : "vertical"}
loadMore={fetchNextPage}
@ -126,7 +128,7 @@ export const InfiniteFetch = <Data,>({
isFetching={isFetching}
loader={[...Array(12)].map((_, i) => (
<Fragment key={i.toString()}>
{(divider === true && i !== 0) ? <HR orientation={horizontal ? "vertical" : "horizontal"} /> : divider}
{Divider && i !== 0 && (Divider === true ? <HR /> : <Divider />)}
{children({ isLoading: true } as any, i)}
</Fragment>
))}
@ -134,10 +136,12 @@ export const InfiniteFetch = <Data,>({
>
{items?.map((item, i) => (
<Fragment key={(item as any).id?.toString()}>
{(divider === true && i !== 0) ? <HR orientation={horizontal ? "vertical" : "horizontal"} /> : divider}
{Divider && i !== 0 && (Divider === true ? <HR /> : <Divider />)}
{children({ ...item, isLoading: false } as any, i)}
</Fragment>
))}
</InfiniteScroll>
);
return Header ? <Header>{list}</Header> : list;
};

View File

@ -48,10 +48,12 @@ export const Fetch = <Data,>({
const { data, error } = useFetch(query);
if (error) return <ErrorView error={error} />;
if (!data)
return (
<>{[...Array(placeholderCount)].map((_, i) => children({ isLoading: true } as any, i))}</>
if (!data) {
const placeholders = [...Array(placeholderCount)].map((_, i) =>
children({ isLoading: true } as any, i),
);
return <>{placeholderCount === 1 ? placeholders[0] : placeholders}</>;
}
if (!isPage<object>(data))
return children(data ? { ...data, isLoading: false } : ({ isLoading: true } as any), 0);
return <>{data.items.map((item, i) => children({ ...item, isLoading: false } as any, i))}</>;