Finalize browse's grid layout

This commit is contained in:
Zoe Roux 2022-12-10 14:42:47 +09:00
parent 47ca25fe1c
commit 24dddc3075
7 changed files with 96 additions and 51 deletions

View File

@ -48,7 +48,6 @@ type Props = WithLoading<{
export const Image = ({ export const Image = ({
src, src,
alt, alt,
fallback,
isLoading: forcedLoading = false, isLoading: forcedLoading = false,
layout, layout,
...props ...props
@ -60,36 +59,38 @@ export const Image = ({
>; >;
}) => { }) => {
const { css } = useYoshiki(); const { css } = useYoshiki();
const [isLoading, setLoading] = useState<boolean>(true); const [state, setState] = useState<"loading" | "errored" | "finished">(
const [source, setSource] = useState(src); src ? "loading" : "errored",
);
// This could be done with a key but this makes the API easier to use.
// This unsures that the state is resetted when the source change (useful for recycler lists.)
const [oldSource, setOldSource] = useState(src);
if (oldSource !== src) {
setState("loading");
setOldSource(src);
}
const border = { borderRadius: 6 } satisfies ImageStyle; const border = { borderRadius: 6 } satisfies ImageStyle;
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border])} />; if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border])} />;
if (!source) return <View {...css([{ bg: (theme) => theme.overlay0 }, layout, border])} />; if (!src || state === "errored")
return <View {...css([{ bg: (theme) => theme.overlay0 }, layout, border])} />;
const nativeProps: ImageProps = const nativeProps = Platform.select<ImageProps>({
Platform.OS === "web" web: {
? { defaultSource: typeof src === "string" ? { uri: src } : Array.isArray(src) ? src[0] : src,
defaultSource: },
typeof source === "string" default: {},
? { uri: source } });
: Array.isArray(source)
? source[0]
: source,
}
: {};
return ( return (
<Skeleton variant="custom" show={isLoading} {...css([layout, border])}> <Skeleton variant="custom" show={state === "loading"} {...css([layout, border])}>
<Img <Img
source={typeof source === "string" ? { uri: source } : source} source={typeof src === "string" ? { uri: src } : src}
accessibilityLabel={alt} accessibilityLabel={alt}
onLoad={() => setLoading(false)} onLoad={() => setState("finished")}
onError={() => { onError={() => setState("errored")}
if (fallback) setSource(fallback);
else setLoading(false);
}}
{...nativeProps} {...nativeProps}
{...css( {...css(
[ [

View File

@ -30,8 +30,17 @@ export * from "./tooltip";
export * from "./utils/nojs"; export * from "./utils/nojs";
import { Dimensions } from "react-native";
import { px } from "yoshiki/native"; import { px } from "yoshiki/native";
export const ts = (spacing: number) => { export const ts = (spacing: number) => {
return px(spacing * 8); return px(spacing * 8);
}; };
export const vw = (spacing: number) => {
return px(spacing * Dimensions.get('window').width / 100);
};
export const vh = (spacing: number) => {
return px(spacing * Dimensions.get('window').height / 100);
};

View File

@ -19,7 +19,7 @@
*/ */
import { ReactNode } from "react"; import { ReactNode } from "react";
import { TextProps } from "react-native"; import { Platform, TextProps } from "react-native";
import { TextLink } from "solito/link"; import { TextLink } from "solito/link";
import { useYoshiki } from "yoshiki/native"; import { useYoshiki } from "yoshiki/native";
@ -35,7 +35,8 @@ export const A = ({
href={href} href={href}
textProps={css( textProps={css(
{ {
fontFamily: theme.fonts.paragraph, // TODO: use a real font here.
fontFamily: Platform.OS === "web" ? theme.fonts.paragraph : undefined,
color: theme.paragraph, color: theme.paragraph,
}, },
props, props,

View File

@ -45,11 +45,11 @@ export const SkeletonCss = () => (
export const Skeleton = ({ export const Skeleton = ({
children, children,
show, show: forcedShow,
variant = "text", variant = "text",
...props ...props
}: Omit<ViewProps, "children"> & { }: Omit<ViewProps, "children"> & {
children?: JSX.Element | boolean | null; children?: JSX.Element | JSX.Element[] | boolean | null;
show?: boolean; show?: boolean;
variant?: "text" | "round" | "custom"; variant?: "text" | "round" | "custom";
}) => { }) => {
@ -57,19 +57,19 @@ export const Skeleton = ({
const [width, setWidth] = useState<number | undefined>(undefined); const [width, setWidth] = useState<number | undefined>(undefined);
const perc = (v: number) => (v / 100) * width!; const perc = (v: number) => (v / 100) * width!;
if (show === undefined && children && children !== true) return children; if (forcedShow === undefined && children && children !== true) return <>{children}</>;
return ( return (
<View <View
{...css( {...css(
[ [
{ {
margin: px(2),
position: "relative", position: "relative",
overflow: "hidden", overflow: "hidden",
borderRadius: px(6), borderRadius: px(6),
}, },
variant === "text" && { variant === "text" && {
margin: px(2),
width: percent(75), width: percent(75),
height: rem(1.2), height: rem(1.2),
}, },
@ -82,10 +82,11 @@ export const Skeleton = ({
> >
<AnimatePresence> <AnimatePresence>
{children} {children}
{show && ( {(forcedShow || !children || children === true) && (
<MotiView <MotiView
key="skeleton" key="skeleton"
animate={{ opacity: "1" }} // No clue why it is a number on mobile and a string on web but /shrug
animate={{ opacity: Platform.OS === "web" ? "1" : 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ type: "timing" }} transition={{ type: "timing" }}
onLayout={(e) => setWidth(e.nativeEvent.layout.width)} onLayout={(e) => setWidth(e.nativeEvent.layout.width)}

View File

@ -19,8 +19,8 @@
*/ */
import { ComponentType, ComponentProps } from "react"; import { ComponentType, ComponentProps } from "react";
import { TextProps } from "react-native"; import { Platform, TextProps } from "react-native";
import { useYoshiki } from "yoshiki/native"; import { rem, useYoshiki } from "yoshiki/native";
import { import {
H1 as EH1, H1 as EH1,
H2 as EH2, H2 as EH2,
@ -43,10 +43,16 @@ const styleText = (
{...css( {...css(
[ [
{ {
fontFamily: type === "header" ? theme.fonts.heading : theme.fonts.paragraph, // TODO: use custom fonts on mobile also.
fontFamily:
Platform.OS === "web"
? type === "header"
? theme.fonts.heading
: theme.fonts.paragraph
: undefined,
color: type === "header" ? theme.heading : theme.paragraph, color: type === "header" ? theme.heading : theme.paragraph,
}, },
type === "sub" && { fontWeight: "300" }, type === "sub" && { fontWeight: "300", opacity: 0.8, fontSize: rem(0.8) },
], ],
props as TextProps, props as TextProps,
)} )}

View File

@ -19,6 +19,7 @@
*/ */
import { A, Skeleton, Poster, ts, P, SubP } from "@kyoo/primitives"; import { A, Skeleton, Poster, ts, P, SubP } from "@kyoo/primitives";
import { Platform, View } from "react-native";
import { percent, px, Stylable, useYoshiki } from "yoshiki/native"; import { percent, px, Stylable, useYoshiki } from "yoshiki/native";
import { WithLoading } from "../fetch"; import { WithLoading } from "../fetch";
@ -39,26 +40,49 @@ export const ItemGrid = ({
const { css } = useYoshiki(); const { css } = useYoshiki();
return ( return (
<A <View
href={href ?? ""} // href={href ?? ""}
{...css( {...css(
{ [
display: "flex", {
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
width: { xs: percent(18), sm: percent(25) }, m: { xs: ts(1), sm: ts(2) },
minWidth: { xs: px(90), sm: px(120) }, },
maxWidth: px(168), // We leave no width on native to fill the list's grid.
m: { xs: ts(1), sm: ts(2) }, Platform.OS === "web" && {
}, width: { xs: percent(18), sm: percent(25) },
minWidth: { xs: px(90), sm: px(120) },
maxWidth: px(168),
},
],
props, props,
)} )}
> >
<Poster src={poster} alt={name} width={percent(100)} /> <Poster src={poster} alt={name} isLoading={isLoading} layout={{ width: percent(100) }} />
<Skeleton width={percent(80)}>{isLoading || <P>{name}</P>}</Skeleton> <Skeleton>
{isLoading || (
<P numberOfLines={1} {...css({ marginY: 0, textAlign: "center" })}>
{name}
</P>
)}
</Skeleton>
{(isLoading || subtitle) && ( {(isLoading || subtitle) && (
<Skeleton width={percent(50)}>{isLoading || <SubP>{subtitle}</SubP>}</Skeleton> <Skeleton {...css({ width: percent(50) })}>
{isLoading || (
<SubP
{...css({
marginTop: 0,
textAlign: "center",
})}
>
{subtitle}
</SubP>
)}
</Skeleton>
)} )}
</A> </View>
); );
}; };
ItemGrid.height = px(250);

View File

@ -28,7 +28,8 @@ import {
getDisplayDate, getDisplayDate,
} from "@kyoo/models"; } from "@kyoo/models";
import { DefaultLayout } from "../layout"; import { DefaultLayout } from "../layout";
import { InfiniteFetch, WithLoading } from "../fetch"; import { WithLoading } from "../fetch";
import { InfiniteFetch } from "../fetch-infinite";
import { ItemGrid } from "./grid"; import { ItemGrid } from "./grid";
import { SortBy, SortOrd, Layout } from "./types"; import { SortBy, SortOrd, Layout } from "./types";
@ -83,6 +84,8 @@ export const BrowsePage: QueryPage<{ slug?: string }> = ({ slug }) => {
<InfiniteFetch <InfiniteFetch
query={query(slug, sortKey, sortOrd)} query={query(slug, sortKey, sortOrd)}
placeholderCount={15} placeholderCount={15}
size={ItemGrid.height}
numColumns={3}
/* sx={{ */ /* sx={{ */
/* display: "flex", */ /* display: "flex", */
/* flexWrap: "wrap", */ /* flexWrap: "wrap", */
@ -90,7 +93,7 @@ export const BrowsePage: QueryPage<{ slug?: string }> = ({ slug }) => {
/* justifyContent: "center", */ /* justifyContent: "center", */
/* }} */ /* }} */
> >
{(item, i) => <ItemGrid key={item?.id ?? i} {...itemMap(item)} />} {(item, key) => <ItemGrid key={key} {...itemMap(item)} />}
</InfiniteFetch> </InfiniteFetch>
</> </>
); );