mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-23 15:30:34 -04:00
Use css grid for infinite lists
This commit is contained in:
parent
0e0bb17ad9
commit
eb351f9bed
@ -51,7 +51,7 @@
|
|||||||
"react-native-svg": "13.9.0",
|
"react-native-svg": "13.9.0",
|
||||||
"react-native-uuid": "^2.0.1",
|
"react-native-uuid": "^2.0.1",
|
||||||
"react-native-video": "^6.0.0-alpha.7",
|
"react-native-video": "^6.0.0-alpha.7",
|
||||||
"yoshiki": "1.2.7"
|
"yoshiki": "1.2.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.22.10",
|
"@babel/core": "^7.22.10",
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
"srt-webvtt": "^2.0.0",
|
"srt-webvtt": "^2.0.0",
|
||||||
"superjson": "^1.13.1",
|
"superjson": "^1.13.1",
|
||||||
"sweetalert2": "^11.7.20",
|
"sweetalert2": "^11.7.20",
|
||||||
"yoshiki": "1.2.7",
|
"yoshiki": "1.2.9",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -24,7 +24,7 @@ import { useYoshiki, px, Stylable } from "yoshiki/native";
|
|||||||
import { Icon } from "./icons";
|
import { Icon } from "./icons";
|
||||||
import { P } from "./text";
|
import { P } from "./text";
|
||||||
import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg";
|
import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg";
|
||||||
import { YoshikiStyle } from "yoshiki/src/type";
|
import { YoshikiStyle } from "yoshiki";
|
||||||
import { ComponentType, forwardRef, RefAttributes } from "react";
|
import { ComponentType, forwardRef, RefAttributes } from "react";
|
||||||
|
|
||||||
const stringToColor = (string: string) => {
|
const stringToColor = (string: string) => {
|
||||||
|
@ -19,9 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useWindowDimensions } from "react-native";
|
import { useWindowDimensions } from "react-native";
|
||||||
import { Breakpoints as YoshikiBreakpoint } from "yoshiki/src/type";
|
import { Breakpoints as YoshikiBreakpoint, isBreakpoints, breakpoints } from "yoshiki/native";
|
||||||
import { isBreakpoints } from "yoshiki/src/utils";
|
|
||||||
import { breakpoints } from "yoshiki/native";
|
|
||||||
|
|
||||||
type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];
|
type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];
|
||||||
export type Breakpoint<T> = T | AtLeastOne<YoshikiBreakpoint<T>>;
|
export type Breakpoint<T> = T | AtLeastOne<YoshikiBreakpoint<T>>;
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
import { KyooImage } from "@kyoo/models";
|
import { KyooImage } from "@kyoo/models";
|
||||||
import { Link, Skeleton, Poster, ts, focusReset, P, SubP } from "@kyoo/primitives";
|
import { Link, Skeleton, Poster, ts, focusReset, P, SubP } from "@kyoo/primitives";
|
||||||
import { ImageStyle, Platform } from "react-native";
|
import { ImageStyle, Platform } from "react-native";
|
||||||
import { percent, px, Stylable, useYoshiki } from "yoshiki/native";
|
import { percent, px, Stylable, Theme, useYoshiki } from "yoshiki/native";
|
||||||
import { Layout, WithLoading } from "../fetch";
|
import { Layout, WithLoading } from "../fetch";
|
||||||
|
|
||||||
export const ItemGrid = ({
|
export const ItemGrid = ({
|
||||||
@ -44,35 +44,27 @@ export const ItemGrid = ({
|
|||||||
<Link
|
<Link
|
||||||
href={href ?? ""}
|
href={href ?? ""}
|
||||||
{...css(
|
{...css(
|
||||||
[
|
{
|
||||||
{
|
flexDirection: "column",
|
||||||
flexDirection: "column",
|
alignItems: "center",
|
||||||
alignItems: "center",
|
width: percent(100),
|
||||||
m: { xs: ts(1), sm: ts(4) },
|
child: {
|
||||||
child: {
|
poster: {
|
||||||
poster: {
|
borderColor: (theme) => theme.background,
|
||||||
borderColor: (theme) => theme.background,
|
borderWidth: px(4),
|
||||||
borderWidth: px(4),
|
borderStyle: "solid",
|
||||||
borderStyle: "solid",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fover: {
|
|
||||||
self: focusReset,
|
|
||||||
poster: {
|
|
||||||
borderColor: (theme) => theme.accent,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
textDecorationLine: "underline",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// We leave no width on native to fill the list's grid.
|
fover: {
|
||||||
Platform.OS === "web" && {
|
self: focusReset,
|
||||||
width: { xs: percent(18), sm: percent(25) },
|
poster: {
|
||||||
minWidth: { xs: px(90), sm: px(120) },
|
borderColor: (theme: Theme) => theme.accent,
|
||||||
maxWidth: px(168),
|
},
|
||||||
|
title: {
|
||||||
|
textDecorationLine: "underline",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
props,
|
props,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -111,5 +103,7 @@ export const ItemGrid = ({
|
|||||||
|
|
||||||
ItemGrid.layout = {
|
ItemGrid.layout = {
|
||||||
size: px(150),
|
size: px(150),
|
||||||
numColumns: { xs: 3, sm: 5, xl: 7 },
|
numColumns: { xs: 3, sm: 4, md: 5, lg: 6, xl: 8 },
|
||||||
|
gap: { xs: ts(1), sm: ts(2), md: ts(4) },
|
||||||
|
layout: "grid",
|
||||||
} satisfies Layout;
|
} satisfies Layout;
|
||||||
|
@ -65,7 +65,6 @@ export const ItemList = ({
|
|||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
height: ItemList.layout.size,
|
height: ItemList.layout.size,
|
||||||
borderRadius: px(6),
|
borderRadius: px(6),
|
||||||
m: ts(1),
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
@ -115,4 +114,4 @@ export const ItemList = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ItemList.layout = { numColumns: 1, size: 300 } satisfies Layout;
|
ItemList.layout = { numColumns: 1, size: 300, layout: "vertical", gap: ts(2) } satisfies Layout;
|
||||||
|
@ -28,7 +28,6 @@ export const InfiniteFetch = <Data, Props, _>({
|
|||||||
query,
|
query,
|
||||||
placeholderCount = 15,
|
placeholderCount = 15,
|
||||||
incremental = false,
|
incremental = false,
|
||||||
horizontal = false,
|
|
||||||
children,
|
children,
|
||||||
layout,
|
layout,
|
||||||
empty,
|
empty,
|
||||||
@ -83,7 +82,7 @@ export const InfiniteFetch = <Data, Props, _>({
|
|||||||
<FlashList
|
<FlashList
|
||||||
renderItem={({ item, index }) => children({ isLoading: false, ...item } as any, index)}
|
renderItem={({ item, index }) => children({ isLoading: false, ...item } as any, index)}
|
||||||
data={hasNextPage !== false ? [...(items || []), ...placeholders] : items}
|
data={hasNextPage !== false ? [...(items || []), ...placeholders] : items}
|
||||||
horizontal={horizontal}
|
horizontal={layout.layout === "horizontal"}
|
||||||
keyExtractor={(item: any) => item.id?.toString()}
|
keyExtractor={(item: any) => item.id?.toString()}
|
||||||
numColumns={numColumns}
|
numColumns={numColumns}
|
||||||
estimatedItemSize={size}
|
estimatedItemSize={size}
|
||||||
|
@ -29,13 +29,13 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Stylable, useYoshiki } from "yoshiki";
|
import { Stylable, useYoshiki, ysMap } from "yoshiki";
|
||||||
import { EmptyView, ErrorView, Layout, WithLoading } from "./fetch";
|
import { EmptyView, ErrorView, Layout, WithLoading } from "./fetch";
|
||||||
|
|
||||||
const InfiniteScroll = <Props,>({
|
const InfiniteScroll = <Props,>({
|
||||||
children,
|
children,
|
||||||
loader,
|
loader,
|
||||||
layout = "vertical",
|
layout,
|
||||||
loadMore,
|
loadMore,
|
||||||
hasMore = true,
|
hasMore = true,
|
||||||
isFetching,
|
isFetching,
|
||||||
@ -45,7 +45,7 @@ const InfiniteScroll = <Props,>({
|
|||||||
}: {
|
}: {
|
||||||
children?: ReactElement | (ReactElement | null)[] | null;
|
children?: ReactElement | (ReactElement | null)[] | null;
|
||||||
loader?: (ReactElement | null)[];
|
loader?: (ReactElement | null)[];
|
||||||
layout?: "vertical" | "horizontal" | "grid";
|
layout: Layout;
|
||||||
loadMore: () => void;
|
loadMore: () => void;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
@ -58,10 +58,11 @@ const InfiniteScroll = <Props,>({
|
|||||||
const onScroll = useCallback(() => {
|
const onScroll = useCallback(() => {
|
||||||
if (!ref.current || !hasMore || isFetching) return;
|
if (!ref.current || !hasMore || isFetching) return;
|
||||||
const scroll =
|
const scroll =
|
||||||
layout === "horizontal"
|
layout.layout === "horizontal"
|
||||||
? ref.current.scrollWidth - ref.current.scrollLeft
|
? ref.current.scrollWidth - ref.current.scrollLeft
|
||||||
: ref.current.scrollHeight - ref.current.scrollTop;
|
: ref.current.scrollHeight - ref.current.scrollTop;
|
||||||
const offset = layout === "horizontal" ? ref.current.offsetWidth : ref.current.offsetHeight;
|
const offset =
|
||||||
|
layout.layout === "horizontal" ? ref.current.offsetWidth : ref.current.offsetHeight;
|
||||||
|
|
||||||
if (scroll <= offset * 1.2) loadMore();
|
if (scroll <= offset * 1.2) loadMore();
|
||||||
}, [hasMore, isFetching, layout, loadMore]);
|
}, [hasMore, isFetching, layout, loadMore]);
|
||||||
@ -78,22 +79,30 @@ const InfiniteScroll = <Props,>({
|
|||||||
{...css(
|
{...css(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
display: "flex",
|
display: "grid",
|
||||||
alignItems: "flex-start",
|
gridAutoRows: "max-content",
|
||||||
overflowX: "hidden",
|
// the as any is due to differencies between css types of native and web (already accounted for in yoshiki)
|
||||||
|
gridGap: layout.gap as any,
|
||||||
|
padding: layout.gap as any,
|
||||||
|
},
|
||||||
|
layout.layout == "vertical" && {
|
||||||
|
gridTemplateColumns: "1fr",
|
||||||
|
alignItems: "stretch",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
},
|
},
|
||||||
layout == "vertical" && {
|
layout.layout == "horizontal" && {
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "stretch",
|
alignItems: "stretch",
|
||||||
|
overflowX: "auto",
|
||||||
|
overflowY: "hidden",
|
||||||
|
gridAutoFlow: "column",
|
||||||
|
gridAutoColumns: ysMap(layout.numColumns, (x) => `${100 / x}%`),
|
||||||
|
gridTemplateRows: "max-content",
|
||||||
},
|
},
|
||||||
layout == "horizontal" && {
|
layout.layout === "grid" && {
|
||||||
flexDirection: "row",
|
gridTemplateColumns: ysMap(layout.numColumns, (x) => `repeat(${x}, 1fr)`),
|
||||||
alignItems: "stretch",
|
|
||||||
},
|
|
||||||
layout === "grid" && {
|
|
||||||
flexWrap: "wrap",
|
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
overflowY: "auto",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
@ -127,7 +136,6 @@ export const InfiniteFetch = <Data, _, HeaderProps>({
|
|||||||
placeholderCount = 15,
|
placeholderCount = 15,
|
||||||
children,
|
children,
|
||||||
layout,
|
layout,
|
||||||
horizontal = false,
|
|
||||||
empty,
|
empty,
|
||||||
divider: Divider = false,
|
divider: Divider = false,
|
||||||
Header,
|
Header,
|
||||||
@ -139,7 +147,6 @@ export const InfiniteFetch = <Data, _, HeaderProps>({
|
|||||||
incremental?: boolean;
|
incremental?: boolean;
|
||||||
placeholderCount?: number;
|
placeholderCount?: number;
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
horizontal?: boolean;
|
|
||||||
children: (
|
children: (
|
||||||
item: Data extends Page<infer Item> ? WithLoading<Item> : WithLoading<Data>,
|
item: Data extends Page<infer Item> ? WithLoading<Item> : WithLoading<Data>,
|
||||||
i: number,
|
i: number,
|
||||||
@ -156,8 +163,6 @@ export const InfiniteFetch = <Data, _, HeaderProps>({
|
|||||||
const { items, error, fetchNextPage, hasNextPage, isFetching } = useInfiniteFetch(query, {
|
const { items, error, fetchNextPage, hasNextPage, isFetching } = useInfiniteFetch(query, {
|
||||||
useErrorBoundary: false,
|
useErrorBoundary: false,
|
||||||
});
|
});
|
||||||
const grid = layout.numColumns !== 1;
|
|
||||||
|
|
||||||
if (incremental && items) oldItems.current = items;
|
if (incremental && items) oldItems.current = items;
|
||||||
|
|
||||||
if (error) return addHeader(Header, <ErrorView error={error} />, headerProps);
|
if (error) return addHeader(Header, <ErrorView error={error} />, headerProps);
|
||||||
@ -168,7 +173,7 @@ export const InfiniteFetch = <Data, _, HeaderProps>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
layout={grid ? "grid" : horizontal ? "horizontal" : "vertical"}
|
layout={layout}
|
||||||
loadMore={fetchNextPage}
|
loadMore={fetchNextPage}
|
||||||
hasMore={hasNextPage!}
|
hasMore={hasNextPage!}
|
||||||
isFetching={isFetching}
|
isFetching={isFetching}
|
||||||
|
@ -23,7 +23,12 @@ import { Breakpoint, P } from "@kyoo/primitives";
|
|||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
|
|
||||||
export type Layout = { numColumns: Breakpoint<number>; size: Breakpoint<number> };
|
export type Layout = {
|
||||||
|
numColumns: Breakpoint<number>;
|
||||||
|
size: Breakpoint<number>;
|
||||||
|
gap: Breakpoint<number | string>;
|
||||||
|
layout: "grid" | "horizontal" | "vertical";
|
||||||
|
};
|
||||||
|
|
||||||
export type WithLoading<Item> =
|
export type WithLoading<Item> =
|
||||||
| (Item & { isLoading: false })
|
| (Item & { isLoading: false })
|
||||||
|
@ -10573,7 +10573,7 @@ __metadata:
|
|||||||
react-native-uuid: ^2.0.1
|
react-native-uuid: ^2.0.1
|
||||||
react-native-video: ^6.0.0-alpha.7
|
react-native-video: ^6.0.0-alpha.7
|
||||||
typescript: ^5.1.6
|
typescript: ^5.1.6
|
||||||
yoshiki: 1.2.7
|
yoshiki: 1.2.9
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@ -14285,7 +14285,7 @@ __metadata:
|
|||||||
sweetalert2: ^11.7.20
|
sweetalert2: ^11.7.20
|
||||||
typescript: ^5.1.6
|
typescript: ^5.1.6
|
||||||
webpack: ^5.88.2
|
webpack: ^5.88.2
|
||||||
yoshiki: 1.2.7
|
yoshiki: 1.2.9
|
||||||
zod: ^3.21.4
|
zod: ^3.21.4
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
@ -14667,9 +14667,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"yoshiki@npm:1.2.7":
|
"yoshiki@npm:1.2.9":
|
||||||
version: 1.2.7
|
version: 1.2.9
|
||||||
resolution: "yoshiki@npm:1.2.7"
|
resolution: "yoshiki@npm:1.2.9"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/inline-style-prefixer": ^5.0.0
|
"@types/inline-style-prefixer": ^5.0.0
|
||||||
"@types/node": 18.x.x
|
"@types/node": 18.x.x
|
||||||
@ -14684,7 +14684,7 @@ __metadata:
|
|||||||
optional: true
|
optional: true
|
||||||
react-native-web:
|
react-native-web:
|
||||||
optional: true
|
optional: true
|
||||||
checksum: 2ebe15f481ad347a90fc8d6bc021d17ef03a8f982b31fa00b8700fa65d27e987044faa3d83d69159becb383ad9aa9ea5523eff3888965e25bc457affe147d361
|
checksum: 1cd681cf9ba241f051c8e09a97b3a67ae5610fbba460ac506127c8d78bbcc069c07c727fb84a15a723a887ad58582bbcf57baba7020642ac6234d38d0c21116f
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user