Adapt staff list to react native (first pass)

This commit is contained in:
Zoe Roux 2022-12-14 01:01:05 +09:00
parent 8c28df9517
commit 26f9cf646b
14 changed files with 157 additions and 228 deletions

View File

@ -57,7 +57,7 @@ const ThemedStack = () => {
headerStyle: {
backgroundColor: theme.appbar,
},
headerTintColor: "#fff",
headerTintColor: theme.colors.white,
headerTitleStyle: {
fontWeight: "bold",
},

View File

@ -1,23 +0,0 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { MovieDetails } from "@kyoo/ui";
export default MovieDetails;

View File

@ -1,59 +0,0 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { Avatar, Box, Skeleton, SxProps, Typography } from "@mui/material";
import { Person } from "~/models/resources/person";
import { Link } from "~/utils/link";
export const PersonAvatar = ({ person, sx }: { person?: Person; sx?: SxProps }) => {
if (!person) {
return (
<Box sx={sx}>
<Skeleton variant="circular" sx={{ width: "100%", aspectRatio: "1/1", height: "unset" }} />
<Typography align="center">
<Skeleton />
</Typography>
<Typography variant="body2" align="center">
<Skeleton />
</Typography>
</Box>
);
}
return (
<Link href={`/person/${person.slug}`} color="inherit" sx={sx}>
<Avatar
src={person.poster!}
alt={person.name}
sx={{ width: "100%", height: "unset", aspectRatio: "1/1" }}
/>
<Typography align="center">{person.name}</Typography>
{person.role && person.type && (
<Typography variant="body2" align="center">
{person.type} ({person.role})
</Typography>
)}
{person.role && !person.type && (
<Typography variant="body2" align="center">
{person.role}
</Typography>
)}
</Link>
);
};

View File

@ -18,26 +18,31 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { View, ViewStyle } from "react-native";
import { View } from "react-native";
import { Image } from "./image";
import { useYoshiki, px } from "yoshiki/native";
import { useYoshiki, px, Stylable } from "yoshiki/native";
import { Icon } from "./icons";
import { Skeleton } from "./skeleton";
export const Avatar = ({
src,
alt,
size = px(24),
isLoading = false,
...props
}: {
src?: string;
alt: string;
src?: string | null;
alt?: string;
size?: number;
}) => {
isLoading?: boolean;
} & Stylable) => {
const { css } = useYoshiki();
if (isLoading) return <Skeleton variant="round" {...css({ width: size, height: size })} />;
return (
<View {...css({ borderRadius: size / 2, width: size, height: size })}>
<View {...css({ borderRadius: size / 2, width: size, height: size }, props)}>
{src ? (
<Image src={src} alt={alt} width={size} height={size} />
<Image src={src} alt={alt} layout={{ width: size, height: size }} />
) : (
<Icon icon="account-circle" size={size} />
)}

View File

@ -18,15 +18,20 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { ComponentType } from "react";
import { View, ViewProps } from "react-native";
import { percent, px, useYoshiki } from "yoshiki/native";
export const Container = (props: ViewProps) => {
export const Container = <AsProps = ViewProps,>({
as,
...props
}: { as?: ComponentType<AsProps> } & AsProps) => {
const { css } = useYoshiki();
const As = as ?? View;
return (
<View
{...css(
<As
{...(css(
{
display: "flex",
paddingHorizontal: px(15),
@ -39,7 +44,7 @@ export const Container = (props: ViewProps) => {
},
},
props,
)}
) as any)}
/>
);
};

View File

@ -19,8 +19,9 @@
*/
import { HR as EHR } from "@expo/html-elements";
import { percent, px, Stylable, useYoshiki } from "yoshiki/native";
import { alpha, ts } from ".";
import { px, Stylable, useYoshiki } from "yoshiki/native";
import { alpha } from "./themes";
import { ts } from "./utils";
export const HR = ({
orientation,

View File

@ -22,7 +22,7 @@ import MIcon from "@expo/vector-icons/MaterialIcons";
import { ComponentProps, ComponentType } from "react";
import { PressableProps } from "react-native";
import { Pressable, px, useYoshiki } from "yoshiki/native";
import { Breakpoint, ts } from ".";
import { Breakpoint, ts } from "./utils";
export type IconProps = {
icon: ComponentProps<typeof MIcon>["name"];

View File

@ -31,13 +31,4 @@ export * from "./container";
export * from "./divider";
export * from "./animated";
export * from "./utils/breakpoints";
export * from "./utils/nojs";
export * from "./utils/head";
import { px } from "yoshiki/native";
export const ts = (spacing: number) => {
return px(spacing * 8);
};
export * from "./utils";

View File

@ -18,7 +18,8 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { ComponentType, ReactNode } from "react";
import { ViewStyle } from "@expo/html-elements/build/primitives/View";
import { ComponentType, Fragment, ReactNode } from "react";
import {
Platform,
TextProps,
@ -26,6 +27,7 @@ import {
TouchableNativeFeedback,
View,
ViewProps,
StyleSheet,
} from "react-native";
import { LinkCore, TextLink } from "solito/link";
import { useYoshiki, Pressable } from "yoshiki/native";
@ -70,26 +72,35 @@ export const Link = ({
}) => {
const { onBlur, onFocus, onPressIn, onPressOut, ...noFocusProps } = props;
const focusProps = { onBlur, onFocus, onPressIn, onPressOut };
const radiusStyle = Platform.select<ViewProps>({
android: {
style: { borderRadius: StyleSheet.flatten(props?.style).borderRadius, overflow: "hidden" },
},
default: {},
});
const Wrapper = radiusStyle ? View : Fragment;
return (
<LinkCore
href={href}
Component={Platform.select<ComponentType>({
web: View,
android: TouchableNativeFeedback,
ios: TouchableOpacity,
default: Pressable,
})}
componentProps={Platform.select<object>({
android: { useForeground: true, ...focusProps },
default: props,
})}
>
{Platform.select<ReactNode>({
android: <View {...noFocusProps}>{children}</View>,
ios: <View {...noFocusProps}>{children}</View>,
default: children,
})}
</LinkCore>
<Wrapper {...radiusStyle}>
<LinkCore
href={href}
Component={Platform.select<ComponentType>({
web: View,
android: TouchableNativeFeedback,
ios: TouchableOpacity,
default: Pressable,
})}
componentProps={Platform.select<object>({
android: { useForeground: true, ...focusProps },
default: props,
})}
>
{Platform.select<ReactNode>({
android: <View {...noFocusProps}>{children}</View>,
ios: <View {...noFocusProps}>{children}</View>,
default: children,
})}
</LinkCore>
</Wrapper>
);
};

View File

@ -31,7 +31,7 @@ import {
P as EP,
LI as ELI,
} from "@expo/html-elements";
import { ts } from ".";
import { ts } from "./utils/spacing";
const styleText = (
Component: ComponentType<ComponentProps<typeof EP>>,
@ -76,7 +76,7 @@ export const LI = ({ children, ...props }: TextProps) => {
const { css } = useYoshiki();
return (
<P accessibilityRole="listitem" {...props}>
<P accessibilityRole={Platform.OS === "web" ? "listitem" : props.accessibilityRole} {...props}>
<Text
{...css({
height: percent(100),

View File

@ -39,11 +39,12 @@ import {
A,
ts,
} from "@kyoo/primitives";
import { ScrollView } from "moti";
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { StyleSheet, View } from "react-native";
import {
Theme,
sm,
md,
px,
min,
@ -229,16 +230,14 @@ const Description = ({
>
{t("show.genre")}:{" "}
{(isLoading ? [...Array(3)] : genres!).map((genre, i) => (
<>
{i !== 0 && ", "}
<Fragment key={genre?.slug ?? i.toString()}>
<P>{i !== 0 && ", "}</P>
{isLoading ? (
<Skeleton key={i} {...css({ width: rem(5) })} />
<Skeleton {...css({ width: rem(5) })} />
) : (
<A key={genre.slug} href={`/genres/${genre.slug}`}>
{genre.name}
</A>
<A href={`/genres/${genre.slug}`}>{genre.name}</A>
)}
</>
</Fragment>
))}
</P>
@ -278,65 +277,52 @@ const Description = ({
);
};
export const ShowHeader = ({
query,
slug,
}: {
query: QueryIdentifier<Show | Movie>;
slug: string;
}) => {
/* const scroll = useScroll(); */
export const Header = ({ query, slug }: { query: QueryIdentifier<Show | Movie>; slug: string }) => {
const { css } = useYoshiki();
// TODO: tweek the navbar color with the theme.
return (
<>
<Navbar {...css({ bg: "transparent" })} />
<Fetch query={query}>
{({ isLoading, ...data }) => (
<Main {...css(StyleSheet.absoluteFillObject)}>
<Head title={data?.name} description={data?.overview} />
{/* TODO: Add a shadow on navbar items */}
{/* TODO: Put the navbar outside of the scrollbox */}
<ImageBackground
src={data?.thumbnail}
alt=""
containerStyle={{
height: {
xs: vh(40),
sm: min(vh(60), px(750)),
md: min(vh(60), px(680)),
lg: vh(70),
},
minHeight: { xs: px(350), sm: px(300), md: px(400), lg: px(600) },
}}
>
<TitleLine
isLoading={isLoading}
slug={slug}
name={data?.name}
date={data ? getDisplayDate(data as any) : undefined}
poster={data?.poster}
studio={data?.studio}
{...css({
marginTop: {
xs: max(vh(20), px(200)),
sm: vh(45),
md: max(vh(30), px(150)),
lg: max(vh(35), px(200)),
},
})}
/>
</ImageBackground>
<Description
<Fetch query={query}>
{({ isLoading, ...data }) => (
<ScrollView>
<Head title={data?.name} description={data?.overview} />
<ImageBackground
src={data?.thumbnail}
alt=""
containerStyle={{
height: {
xs: vh(40),
sm: min(vh(60), px(750)),
md: min(vh(60), px(680)),
lg: vh(70),
},
minHeight: { xs: px(350), sm: px(300), md: px(400), lg: px(600) },
}}
>
<TitleLine
isLoading={isLoading}
overview={data?.overview}
genres={data?.genres}
{...css({ paddingTop: { xs: 0, md: ts(2) } })}
slug={slug}
name={data?.name}
date={data ? getDisplayDate(data as any) : undefined}
poster={data?.poster}
studio={data?.studio}
{...css({
marginTop: {
xs: max(vh(20), px(200)),
sm: vh(45),
md: max(vh(30), px(150)),
lg: max(vh(35), px(200)),
},
})}
/>
</Main>
)}
</Fetch>
</>
</ImageBackground>
<Description
isLoading={isLoading}
overview={data?.overview}
genres={data?.genres}
{...css({ paddingTop: { xs: 0, md: ts(2) } })}
/>
</ScrollView>
)}
</Fetch>
);
};

View File

@ -19,7 +19,10 @@
*/
import { Movie, MovieP, QueryIdentifier, QueryPage } from "@kyoo/models";
import { ShowHeader } from "./header";
import { Navbar } from "../navbar";
import { DefaultLayout } from "../layout";
import { Header } from "./header";
import { Staff } from "./staff";
const query = (slug: string): QueryIdentifier<Movie> => ({
parser: MovieP,
@ -32,16 +35,16 @@ const query = (slug: string): QueryIdentifier<Movie> => ({
export const MovieDetails: QueryPage<{ slug: string }> = ({ slug }) => {
return (
<>
{/* <Head> */}
{/* <title>{makeTitle(data?.name)}</title> */}
{/* <meta name="description" content={data?.overview!} /> */}
{/* </Head> */}
<ShowHeader slug={slug} query={query(slug)} />
{/* <ShowStaff slug={slug} /> */}
<Header slug={slug} query={query(slug)} />
{/* <Staff slug={slug} /> */}
</>
);
};
MovieDetails.getFetchUrls = ({ slug }) => [//query(slug),
// ShowStaff.query(slug), Navbar.query()
MovieDetails.getFetchUrls = ({ slug }) => [
query(slug),
// ShowStaff.query(slug),
Navbar.query(),
];
MovieDetails.getLayout = DefaultLayout;

View File

@ -18,30 +18,40 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
export {}
// export const ShowStaff = ({ slug }: { slug: string }) => {
// const { items, isError, error } = useInfiniteFetch(ShowStaff.query(slug));
// const { t } = useTranslation("browse");
import { Person, PersonP, QueryIdentifier } from "@kyoo/models";
import { useTranslation } from "react-i18next";
import { InfiniteFetch } from "../fetch-infinite";
import { PersonAvatar } from "./person";
// // TODO: handle infinite scroll
export const Staff = ({ slug }: { slug: string }) => {
const { t } = useTranslation();
// if (isError) return <ErrorComponent {...error} />;
// TODO: handle infinite scroll
// return (
// <HorizontalList title={t("show.staff")} noContent={t("show.staff-none")}>
// {(items ?? [...Array(20)]).map((x, i) => (
// <PersonAvatar
// key={x ? x.id : i}
// person={x}
// sx={{ width: { xs: "7rem", lg: "10rem" }, flexShrink: 0, px: 2 }}
// />
// ))}
// </HorizontalList>
// );
// };
return (
<InfiniteFetch
query={Staff.query(slug)}
layout={{ numColumns: 0, size: PersonAvatar.width }}
placeholderCount={20}
>
{/* <HorizontalList title={t("show.staff")} noContent={t("show.staff-none")}> */}
{(item, key) => (
<PersonAvatar
key={key}
isLoading={item.isLoading}
slug={item?.slug}
name={item?.name}
role={item?.type ? `${item?.type} (${item?.role})` : item?.role}
poster={item?.poster}
// sx={{ width: { xs: "7rem", lg: "10rem" }, flexShrink: 0, px: 2 }}
/>
)}
</InfiniteFetch>
);
};
// ShowStaff.query = (slug: string): QueryIdentifier<Person> => ({
// parser: PersonP,
// path: ["shows", slug, "people"],
// infinite: true,
// });
Staff.query = (slug: string): QueryIdentifier<Person> => ({
parser: PersonP,
path: ["shows", slug, "people"],
infinite: true,
});

View File

@ -20,6 +20,7 @@
import { Page, QueryIdentifier, useFetch, KyooErrors } from "@kyoo/models";
import { Breakpoint, P } from "@kyoo/primitives";
import { Fragment } from "react";
import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
@ -47,14 +48,12 @@ export const Fetch = <Data,>({
const { data, error } = useFetch(query);
if (error) return <ErrorView error={error} />;
if (placeholderCount === 1 || !isPage<object>(data))
return children(data ? { ...data, isLoading: false } : ({ isLoading: true } as any), 0);
if (!data)
return (
<>{[...Array(placeholderCount)].map((_, i) => children({ isLoading: true } as any, i))}</>
);
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))}</>;
};