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", display: "flex",
paddingHorizontal: px(15), paddingHorizontal: px(15),
marginHorizontal: "auto", alignSelf: "center",
width: { width: {
xs: percent(100), xs: percent(100),
sm: px(540), sm: px(540),

View File

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

View File

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

View File

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

View File

@ -40,7 +40,7 @@ import {
} from "@kyoo/primitives"; } from "@kyoo/primitives";
import { Fragment } from "react"; import { Fragment } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { View } from "react-native"; import { Platform, View } from "react-native";
import { import {
Theme, Theme,
md, md,
@ -55,8 +55,8 @@ import {
Stylable, Stylable,
} from "yoshiki/native"; } from "yoshiki/native";
import { Fetch } from "../fetch"; import { Fetch } from "../fetch";
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-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" import Theaters from "@material-symbols/svg-400/rounded/theaters-fill.svg";
const TitleLine = ({ const TitleLine = ({
isLoading, isLoading,
@ -100,7 +100,10 @@ const TitleLine = ({
layout={{ layout={{
width: { xs: percent(50), md: percent(25) }, 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 <View
{...css({ {...css({

View File

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

View File

@ -19,11 +19,27 @@
*/ */
import { QueryIdentifier, QueryPage, Show, ShowP } from "@kyoo/models"; import { QueryIdentifier, QueryPage, Show, ShowP } from "@kyoo/models";
import { Platform, ScrollView } from "react-native"; import { Platform, ScrollView, View, ViewProps } from "react-native";
import { useYoshiki } from "yoshiki/native"; import { percent, useYoshiki, vh } from "yoshiki/native";
import { TransparentLayout } from "../layout"; import { TransparentLayout } from "../layout";
import { SeasonTab } from "./season"; import { EpisodeList, SeasonTab } from "./season";
import { Header } from "./header"; 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> => ({ const query = (slug: string): QueryIdentifier<Show> => ({
parser: ShowP, parser: ShowP,
@ -34,14 +50,39 @@ const query = (slug: string): QueryIdentifier<Show> => ({
}); });
export const ShowDetails: QueryPage<{ slug: string; season: string }> = ({ slug, season }) => { export const ShowDetails: QueryPage<{ slug: string; season: string }> = ({ slug, season }) => {
const { css } = useYoshiki(); const { css, theme } = useYoshiki();
return ( const ShowHeader = ({ children, ...props }: ViewProps) => (
<ScrollView {...css(Platform.OS === "web" && { overflow: "overlay" as any })}> <View
{...css(
[
{ bg: (theme) => theme.background },
Platform.OS === "web" && { flexGrow: 1, flexShrink: 1, overflow: "overlay" as any },
],
props,
)}
>
<Header slug={slug} query={query(slug)} /> <Header slug={slug} query={query(slug)} />
{/* <Staff slug={slug} /> */} {/* <Staff slug={slug} /> */}
<SeasonTab slug={slug} season={season} /> <SvgWave
</ScrollView> 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 { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models";
import { useBreakpointMap, HR } from "@kyoo/primitives"; import { useBreakpointMap, HR } from "@kyoo/primitives";
import { FlashList } from "@shopify/flash-list"; import { FlashList } from "@shopify/flash-list";
import { ReactElement } from "react"; import { ComponentType, ReactElement } from "react";
import { EmptyView, ErrorView, Layout, WithLoading } from "./fetch"; import { EmptyView, ErrorView, Layout, WithLoading } from "./fetch";
export const InfiniteFetch = <Data,>({ export const InfiniteFetch = <Data,>({
@ -32,6 +32,7 @@ export const InfiniteFetch = <Data,>({
layout, layout,
empty, empty,
divider = false, divider = false,
Header,
...props ...props
}: { }: {
query: QueryIdentifier<Data>; query: QueryIdentifier<Data>;
@ -43,7 +44,8 @@ export const InfiniteFetch = <Data,>({
i: number, i: number,
) => ReactElement | null; ) => ReactElement | null;
empty?: string | JSX.Element; empty?: string | JSX.Element;
divider?: boolean | JSX.Element; divider?: boolean | ComponentType;
Header?: ComponentType<{ children: JSX.Element }>;
}): JSX.Element | null => { }): JSX.Element | null => {
if (!query.infinite) console.warn("A non infinite query was passed to an InfiniteFetch."); 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} />; return <EmptyView message={empty} />;
} }
const placeholders = [
...Array(items ? numColumns - (items.length % numColumns) + numColumns : placeholderCount),
].map((_, i) => ({ id: `gen${i}`, isLoading: true } as Data));
return ( return (
<FlashList <FlashList
renderItem={({ item, index }) => ( renderItem={({ item, index }) => children({ isLoading: false, ...item } as any, index)}
<> data={hasNextPage !== false ? [...(items || []), ...placeholders] : items}
{(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
}
horizontal={horizontal} horizontal={horizontal}
keyExtractor={(item: any) => item.id?.toString()} keyExtractor={(item: any) => item.id?.toString()}
numColumns={numColumns} numColumns={numColumns}
@ -85,6 +75,8 @@ export const InfiniteFetch = <Data,>({
onEndReachedThreshold={0.5} onEndReachedThreshold={0.5}
onRefresh={refetch} onRefresh={refetch}
refreshing={isRefetching} refreshing={isRefetching}
ItemSeparatorComponent={divider === true ? HR : divider || null}
ListHeaderComponent={Header}
{...props} {...props}
/> />
); );

View File

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

View File

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