/* * 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 . */ import { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models"; import { HR } from "@kyoo/primitives"; import { ComponentProps, ComponentType, Fragment, isValidElement, ReactElement, useCallback, useEffect, useRef, } from "react"; import { Stylable, nativeStyleToCss, useYoshiki, ysMap } from "yoshiki"; import { EmptyView, ErrorView, Layout, WithLoading, addHeader } from "./fetch"; import type { ContentStyle } from "@shopify/flash-list"; const InfiniteScroll = ({ children, loader, layout, loadMore, hasMore = true, isFetching, Header, headerProps, fetchMore = true, contentContainerStyle, ...props }: { children?: ReactElement | (ReactElement | null)[] | null; loader?: (ReactElement | null)[]; layout: Layout; loadMore: () => void; hasMore: boolean; isFetching: boolean; Header?: ComponentType | ReactElement; headerProps?: Props; fetchMore?: boolean; contentContainerStyle?: ContentStyle; } & Stylable) => { const ref = useRef(null); const { css } = useYoshiki(); const onScroll = useCallback(() => { if (!ref.current || !hasMore || isFetching || !fetchMore) return; const scroll = layout.layout === "horizontal" ? ref.current.scrollWidth - ref.current.scrollLeft : ref.current.scrollHeight - ref.current.scrollTop; const offset = layout.layout === "horizontal" ? ref.current.offsetWidth : ref.current.offsetHeight; // Load more if less than 3 element's worth of scroll is left if (scroll <= offset * 3) loadMore(); }, [hasMore, isFetching, layout, loadMore, fetchMore]); const scrollProps = { ref, onScroll }; // Automatically trigger a scroll check on start and after a fetch end in case the user is already // at the bottom of the page or if there is no scroll bar (ultrawide or something like that) useEffect(() => { onScroll(); }, [isFetching, onScroll]); const list = (props: object) => (
`${100 / x}%`), gridTemplateRows: "max-content", paddingX: layout.gap as any, }, layout.layout === "grid" && { gridTemplateColumns: ysMap(layout.numColumns, (x) => `repeat(${x}, 1fr)`), justifyContent: "center", alignItems: "flex-start", overflowY: "auto", padding: layout.gap as any, }, contentContainerStyle as any, ], nativeStyleToCss(props), )} > {children} {isFetching && loader}
); if (!Header) return list({ ...scrollProps, ...props }); if (!isValidElement(Header)) return ( // @ts-ignore
{list(props)}
); return ( <> {Header} {list({ ...scrollProps, ...props })} ); }; export const InfiniteFetchList = ({ query, incremental = false, placeholderCount = 2, children, layout, empty, divider: Divider = false, Header, headerProps, getItemType, nested, ...props }: { query: ReturnType>; incremental?: boolean; placeholderCount?: number; layout: Layout; children: ( item: Data extends Page ? WithLoading : WithLoading, i: number, ) => ReactElement | null; empty?: string | JSX.Element; divider?: boolean | ComponentType; Header?: ComponentType<{ children: JSX.Element } & HeaderProps> | ReactElement; headerProps: HeaderProps; getItemType?: (item: WithLoading, index: number) => Kind; getItemSize?: (kind: Kind) => number; fetchMore?: boolean; contentContainerStyle?: ContentStyle; nested?: boolean; }): JSX.Element | null => { const oldItems = useRef(); const { items, error, fetchNextPage, hasNextPage, isFetching } = query; if (incremental && items) oldItems.current = items; if (error) return addHeader(Header, , headerProps); if (empty && items && items.length === 0) { if (typeof empty !== "string") return addHeader(Header, empty, headerProps); return addHeader(Header, , headerProps); } return ( ( {Divider && i !== 0 && (Divider === true ?
: )} {children({ isLoading: true } as any, i)}
))} Header={Header} headerProps={headerProps} {...props} > {(items ?? oldItems.current)?.map((item, i) => ( {Divider && i !== 0 && (Divider === true ?
: )} {children({ ...item, isLoading: false } as any, i)}
))}
); }; export const InfiniteFetch = ({ query, ...props }: { query: QueryIdentifier<_, Data>; } & Omit>, "query">) => { if (!query.infinite) console.warn("A non infinite query was passed to an InfiniteFetch."); const ret = useInfiniteFetch(query); return ; };