mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-03 13:44:33 -04:00
Finalize browse's grid layout
This commit is contained in:
parent
47ca25fe1c
commit
24dddc3075
@ -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(
|
||||||
[
|
[
|
||||||
|
@ -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);
|
||||||
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -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)}
|
||||||
|
@ -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,
|
||||||
)}
|
)}
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user