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: { headerStyle: {
backgroundColor: theme.appbar, backgroundColor: theme.appbar,
}, },
headerTintColor: "#fff", headerTintColor: theme.colors.white,
headerTitleStyle: { headerTitleStyle: {
fontWeight: "bold", 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/>. * 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 { Image } from "./image";
import { useYoshiki, px } from "yoshiki/native"; import { useYoshiki, px, Stylable } from "yoshiki/native";
import { Icon } from "./icons"; import { Icon } from "./icons";
import { Skeleton } from "./skeleton";
export const Avatar = ({ export const Avatar = ({
src, src,
alt, alt,
size = px(24), size = px(24),
isLoading = false,
...props
}: { }: {
src?: string; src?: string | null;
alt: string; alt?: string;
size?: number; size?: number;
}) => { isLoading?: boolean;
} & Stylable) => {
const { css } = useYoshiki(); const { css } = useYoshiki();
if (isLoading) return <Skeleton variant="round" {...css({ width: size, height: size })} />;
return ( return (
<View {...css({ borderRadius: size / 2, width: size, height: size })}> <View {...css({ borderRadius: size / 2, width: size, height: size }, props)}>
{src ? ( {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} /> <Icon icon="account-circle" size={size} />
)} )}

View File

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

View File

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

View File

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

View File

@ -31,13 +31,4 @@ export * from "./container";
export * from "./divider"; export * from "./divider";
export * from "./animated"; export * from "./animated";
export * from "./utils";
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);
};

View File

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

View File

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

View File

@ -39,11 +39,12 @@ import {
A, A,
ts, ts,
} from "@kyoo/primitives"; } from "@kyoo/primitives";
import { ScrollView } from "moti";
import { Fragment } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { StyleSheet, View } from "react-native"; import { StyleSheet, View } from "react-native";
import { import {
Theme, Theme,
sm,
md, md,
px, px,
min, min,
@ -229,16 +230,14 @@ const Description = ({
> >
{t("show.genre")}:{" "} {t("show.genre")}:{" "}
{(isLoading ? [...Array(3)] : genres!).map((genre, i) => ( {(isLoading ? [...Array(3)] : genres!).map((genre, i) => (
<> <Fragment key={genre?.slug ?? i.toString()}>
{i !== 0 && ", "} <P>{i !== 0 && ", "}</P>
{isLoading ? ( {isLoading ? (
<Skeleton key={i} {...css({ width: rem(5) })} /> <Skeleton {...css({ width: rem(5) })} />
) : ( ) : (
<A key={genre.slug} href={`/genres/${genre.slug}`}> <A href={`/genres/${genre.slug}`}>{genre.name}</A>
{genre.name}
</A>
)} )}
</> </Fragment>
))} ))}
</P> </P>
@ -278,65 +277,52 @@ const Description = ({
); );
}; };
export const ShowHeader = ({ export const Header = ({ query, slug }: { query: QueryIdentifier<Show | Movie>; slug: string }) => {
query,
slug,
}: {
query: QueryIdentifier<Show | Movie>;
slug: string;
}) => {
/* const scroll = useScroll(); */
const { css } = useYoshiki(); const { css } = useYoshiki();
// TODO: tweek the navbar color with the theme.
return ( return (
<> <Fetch query={query}>
<Navbar {...css({ bg: "transparent" })} /> {({ isLoading, ...data }) => (
<Fetch query={query}> <ScrollView>
{({ isLoading, ...data }) => ( <Head title={data?.name} description={data?.overview} />
<Main {...css(StyleSheet.absoluteFillObject)}> <ImageBackground
<Head title={data?.name} description={data?.overview} /> src={data?.thumbnail}
{/* TODO: Add a shadow on navbar items */} alt=""
{/* TODO: Put the navbar outside of the scrollbox */} containerStyle={{
<ImageBackground height: {
src={data?.thumbnail} xs: vh(40),
alt="" sm: min(vh(60), px(750)),
containerStyle={{ md: min(vh(60), px(680)),
height: { lg: vh(70),
xs: vh(40), },
sm: min(vh(60), px(750)), minHeight: { xs: px(350), sm: px(300), md: px(400), lg: px(600) },
md: min(vh(60), px(680)), }}
lg: vh(70), >
}, <TitleLine
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
isLoading={isLoading} isLoading={isLoading}
overview={data?.overview} slug={slug}
genres={data?.genres} name={data?.name}
{...css({ paddingTop: { xs: 0, md: ts(2) } })} 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> </ImageBackground>
)} <Description
</Fetch> 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 { 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> => ({ const query = (slug: string): QueryIdentifier<Movie> => ({
parser: MovieP, parser: MovieP,
@ -32,16 +35,16 @@ const query = (slug: string): QueryIdentifier<Movie> => ({
export const MovieDetails: QueryPage<{ slug: string }> = ({ slug }) => { export const MovieDetails: QueryPage<{ slug: string }> = ({ slug }) => {
return ( return (
<> <>
{/* <Head> */} <Header slug={slug} query={query(slug)} />
{/* <title>{makeTitle(data?.name)}</title> */} {/* <Staff slug={slug} /> */}
{/* <meta name="description" content={data?.overview!} /> */}
{/* </Head> */}
<ShowHeader slug={slug} query={query(slug)} />
{/* <ShowStaff slug={slug} /> */}
</> </>
); );
}; };
MovieDetails.getFetchUrls = ({ slug }) => [//query(slug), MovieDetails.getFetchUrls = ({ slug }) => [
// ShowStaff.query(slug), Navbar.query() 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/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
export {} import { Person, PersonP, QueryIdentifier } from "@kyoo/models";
// export const ShowStaff = ({ slug }: { slug: string }) => { import { useTranslation } from "react-i18next";
// const { items, isError, error } = useInfiniteFetch(ShowStaff.query(slug)); import { InfiniteFetch } from "../fetch-infinite";
// const { t } = useTranslation("browse"); 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 ( return (
// <HorizontalList title={t("show.staff")} noContent={t("show.staff-none")}> <InfiniteFetch
// {(items ?? [...Array(20)]).map((x, i) => ( query={Staff.query(slug)}
// <PersonAvatar layout={{ numColumns: 0, size: PersonAvatar.width }}
// key={x ? x.id : i} placeholderCount={20}
// person={x} >
// sx={{ width: { xs: "7rem", lg: "10rem" }, flexShrink: 0, px: 2 }} {/* <HorizontalList title={t("show.staff")} noContent={t("show.staff-none")}> */}
// /> {(item, key) => (
// ))} <PersonAvatar
// </HorizontalList> 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> => ({ Staff.query = (slug: string): QueryIdentifier<Person> => ({
// parser: PersonP, parser: PersonP,
// path: ["shows", slug, "people"], path: ["shows", slug, "people"],
// infinite: true, infinite: true,
// }); });

View File

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