mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Make episode list work on mobile
This commit is contained in:
parent
eabf5e1faf
commit
2ac4c434f5
@ -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),
|
||||
|
@ -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 (
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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({
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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))}</>;
|
||||
|
Loading…
x
Reference in New Issue
Block a user