Make the main scroll instead of the body. Support other layouts

This commit is contained in:
Zoe Roux 2022-12-10 23:14:57 +09:00
parent bb63340555
commit 3b29e1a87a
9 changed files with 108 additions and 25 deletions

View File

@ -66,6 +66,10 @@ const GlobalCssTheme = () => {
*:hover::-webkit-scrollbar-thumb { *:hover::-webkit-scrollbar-thumb {
background-color: rgb(134, 127, 127); background-color: rgb(134, 127, 127);
} }
#__next {
height: 100vh;
}
`}</style> `}</style>
<WebTooltip theme={theme} /> <WebTooltip theme={theme} />
<SkeletonCss /> <SkeletonCss />

View File

@ -28,6 +28,7 @@ export * from "./image";
export * from "./skeleton"; export * from "./skeleton";
export * from "./tooltip"; export * from "./tooltip";
export * from "./utils/breakpoints";
export * from "./utils/nojs"; export * from "./utils/nojs";
import { Dimensions } from "react-native"; import { Dimensions } from "react-native";

View File

@ -0,0 +1,62 @@
/*
* 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 { useWindowDimensions } from "react-native";
import { AtLeastOne, Breakpoints as YoshikiBreakpoint } from "yoshiki/dist/type";
import { isBreakpoints } from "yoshiki/dist/utils";
import { breakpoints } from "yoshiki/native";
export type Breakpoint<T> = T | AtLeastOne<YoshikiBreakpoint<T>>;
// copied from yoshiki.
const useBreakpoint = () => {
const { width } = useWindowDimensions();
const idx = Object.values(breakpoints).findIndex((x) => width <= x);
if (idx === -1) return 0;
return idx - 1;
};
const getBreakpointValue = <T>(value: Breakpoint<T>, breakpoint: number): T => {
if (!isBreakpoints(value)) return value;
const bpKeys = Object.keys(breakpoints) as Array<keyof YoshikiBreakpoint<T>>;
for (let i = breakpoint; i >= 0; i--) {
if (bpKeys[i] in value) {
const val = value[bpKeys[i]];
if (val) return val;
}
}
// This should never be reached.
return undefined!;
};
export const useBreakpointValue = <T>(value: Breakpoint<T>): T => {
const breakpoint = useBreakpoint();
return getBreakpointValue(value, breakpoint);
};
export const useBreakpointMap = <T extends Record<string, unknown>>(
value: T,
): { [key in keyof T]: T[key] extends Breakpoint<infer V> ? V : T } => {
const breakpoint = useBreakpoint();
// @ts-ignore
return Object.fromEntries(
Object.entries(value).map(([key, val]) => [key, getBreakpointValue(val, breakpoint)]),
);
};

View File

@ -21,7 +21,7 @@
import { Link, Skeleton, Poster, ts, P, SubP } from "@kyoo/primitives"; import { Link, Skeleton, Poster, ts, P, SubP } from "@kyoo/primitives";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { percent, px, Stylable, useYoshiki } from "yoshiki/native"; import { percent, px, Stylable, useYoshiki } from "yoshiki/native";
import { WithLoading } from "../fetch"; import { Layout, WithLoading } from "../fetch";
export const ItemGrid = ({ export const ItemGrid = ({
href, href,
@ -85,4 +85,7 @@ export const ItemGrid = ({
); );
}; };
ItemGrid.height = px(250); ItemGrid.layout = {
size: px(150),
numColumns: { xs: 3, md: 5, xl: 7 },
} satisfies Layout;

View File

@ -31,6 +31,7 @@ import { DefaultLayout } from "../layout";
import { WithLoading } from "../fetch"; import { WithLoading } from "../fetch";
import { InfiniteFetch } from "../fetch-infinite"; import { InfiniteFetch } from "../fetch-infinite";
import { ItemGrid } from "./grid"; import { ItemGrid } from "./grid";
import { ItemList } from "./list";
import { SortBy, SortOrd, Layout } from "./types"; import { SortBy, SortOrd, Layout } from "./types";
const itemMap = (item: WithLoading<LibraryItem>): WithLoading<ComponentProps<typeof ItemGrid>> => { const itemMap = (item: WithLoading<LibraryItem>): WithLoading<ComponentProps<typeof ItemGrid>> => {
@ -71,6 +72,8 @@ export const BrowsePage: QueryPage<{ slug?: string }> = ({ slug }) => {
const [sortOrd, setSortOrd] = useState(SortOrd.Asc); const [sortOrd, setSortOrd] = useState(SortOrd.Asc);
const [layout, setLayout] = useState(Layout.Grid); const [layout, setLayout] = useState(Layout.Grid);
const LayoutComponent = layout === Layout.Grid ? ItemGrid : ItemList;
return ( return (
<> <>
{/* <BrowseSettings */} {/* <BrowseSettings */}
@ -84,16 +87,9 @@ 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} layout={LayoutComponent.layout}
numColumns={3}
/* sx={{ */
/* display: "flex", */
/* flexWrap: "wrap", */
/* alignItems: "flex-start", */
/* justifyContent: "center", */
/* }} */
> >
{(item, key) => <ItemGrid key={key} {...itemMap(item)} />} {(item, key) => <LayoutComponent key={key} {...itemMap(item)} />}
</InfiniteFetch> </InfiniteFetch>
</> </>
); );

View File

@ -19,30 +19,30 @@
*/ */
import { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models"; import { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models";
import { useBreakpointMap } from "@kyoo/primitives";
import { FlashList } from "@shopify/flash-list"; import { FlashList } from "@shopify/flash-list";
import { ReactElement } from "react"; import { ReactElement } from "react";
import { ErrorView, WithLoading } from "./fetch"; import { ErrorView, Layout, WithLoading } from "./fetch";
export const InfiniteFetch = <Data,>({ export const InfiniteFetch = <Data,>({
query, query,
placeholderCount = 15, placeholderCount = 15,
children, children,
size, layout,
numColumns,
...props ...props
}: { }: {
query: QueryIdentifier<Data>; query: QueryIdentifier<Data>;
placeholderCount?: number; placeholderCount?: number;
numColumns: number; layout: Layout;
children: ( children: (
item: Data extends Page<infer Item> ? WithLoading<Item> : WithLoading<Data>, item: Data extends Page<infer Item> ? WithLoading<Item> : WithLoading<Data>,
key: string | undefined, key: string | undefined,
i: number, i: number,
) => ReactElement | null; ) => ReactElement | null;
size: number;
}): JSX.Element | null => { }): JSX.Element | null => {
if (!query.infinite) console.warn("A non infinite query was passed to an InfiniteFetch."); if (!query.infinite) console.warn("A non infinite query was passed to an InfiniteFetch.");
const { numColumns, size } = useBreakpointMap(layout);
const { items, error, fetchNextPage, hasNextPage, refetch, isRefetching } = const { items, error, fetchNextPage, hasNextPage, refetch, isRefetching } =
useInfiniteFetch(query); useInfiniteFetch(query);

View File

@ -19,19 +19,22 @@
*/ */
import { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models"; import { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models";
import { useBreakpointValue } from "@kyoo/primitives";
import { ReactElement } from "react"; import { ReactElement } from "react";
import InfiniteScroll from "react-infinite-scroll-component"; import InfiniteScroll from "react-infinite-scroll-component";
import { useYoshiki } from "yoshiki"; import { useYoshiki } from "yoshiki";
import { ErrorView, WithLoading } from "./fetch"; import { ErrorView, Layout, WithLoading } from "./fetch";
export const InfiniteFetch = <Data,>({ export const InfiniteFetch = <Data,>({
query, query,
placeholderCount = 15, placeholderCount = 15,
children, children,
layout,
...props ...props
}: { }: {
query: QueryIdentifier<Data>; query: QueryIdentifier<Data>;
placeholderCount?: number; placeholderCount?: number;
layout: Layout;
children: ( children: (
item: Data extends Page<infer Item> ? WithLoading<Item> : WithLoading<Data>, item: Data extends Page<infer Item> ? WithLoading<Item> : WithLoading<Data>,
key: string | undefined, key: string | undefined,
@ -41,23 +44,33 @@ export const InfiniteFetch = <Data,>({
if (!query.infinite) console.warn("A non infinite query was passed to an InfiniteFetch."); if (!query.infinite) console.warn("A non infinite query was passed to an InfiniteFetch.");
const { items, error, fetchNextPage, hasNextPage } = useInfiniteFetch(query); const { items, error, fetchNextPage, hasNextPage } = useInfiniteFetch(query);
const { numColumns } = useBreakpointValue(layout);
const { css } = useYoshiki(); const { css } = useYoshiki();
if (error) return <ErrorView error={error} />; if (error) return <ErrorView error={error} />;
return ( return (
<InfiniteScroll <InfiniteScroll
scrollableTarget="main" // Default to the main element for the scroll.
dataLength={items?.length ?? 0} dataLength={items?.length ?? 0}
next={fetchNextPage} next={fetchNextPage}
hasMore={hasNextPage!} hasMore={hasNextPage!}
loader={[...Array(12)].map((_, i) => children({ isLoading: true } as any, i.toString(), i))} loader={[...Array(12)].map((_, i) => children({ isLoading: true } as any, i.toString(), i))}
{...css( {...css(
{ [
display: "flex", {
flexWrap: "wrap", display: "flex",
alignItems: "flex-start", alignItems: "flex-start",
justifyContent: "center", justifyContent: "center",
}, },
numColumns === 1 && {
flexDirection: "column",
},
numColumns !== 1 && {
flexWrap: "wrap",
},
],
props, props,
)} )}
> >

View File

@ -19,10 +19,12 @@
*/ */
import { Page, QueryIdentifier, useFetch, KyooErrors } from "@kyoo/models"; import { Page, QueryIdentifier, useFetch, KyooErrors } from "@kyoo/models";
import { P } from "@kyoo/primitives"; 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 WithLoading<Item> = export type WithLoading<Item> =
| (Item & { isLoading: false }) | (Item & { isLoading: false })
| (Partial<Item> & { isLoading: true }); | (Partial<Item> & { isLoading: true });

View File

@ -28,7 +28,9 @@ export const DefaultLayout = (page: ReactElement) => {
return ( return (
<> <>
<Navbar /> <Navbar />
<main {...css({ flex: 1, display: "flex" })}>{page}</main> <main id="main" {...css({ display: "flex", flexGrow: 1, flexShrink: 1, overflow: "auto" })}>
{page}
</main>
</> </>
); );
}; };