Rework Fetch component

This commit is contained in:
Zoe Roux 2025-02-02 21:32:43 +01:00
parent 930fb3edfc
commit 43e1dfd2cd
No known key found for this signature in database
10 changed files with 99 additions and 643 deletions

View File

@ -1,19 +1,34 @@
import { Text, View } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { LibraryItem, LibraryItemP, type News, NewsP } from "~/models";
import type { QueryIdentifier } from "~/query/index";
export default function MyApp() {
export async function loader() {
await prefetchQuery(Header.query());
}
export default function Header() {
const { css } = useYoshiki();
return (
<View
{...css({
flex: 1,
justifyContent: "center",
alignItems: "center",
minHeight: "100%",
})}
>
<Text>Hello from One</Text>
</View>
<Fetch
query={NewsList.query()}
layout={{ ...ItemGrid.layout, layout: "horizontal" }}
getItemType={(x, i) => (x?.kind === "movie" || (!x && i % 2) ? "movie" : "episode")}
getItemSize={(kind) => (kind === "episode" ? 2 : 1)}
empty={t("home.none")}
Render={({ item }) => {
<Text>{item.name}</Text>;
}}
Loader={({ index }) => (index % 2 ? <EpisodeBox.Loader /> : <ItemGrid.Loader />)}
/>
);
}
Header.query = (): QueryIdentifier<LibraryItem> => ({
parser: LibraryItemP,
path: ["items", "random"],
params: {
fields: ["firstEpisode"],
},
});

File diff suppressed because it is too large Load Diff

View File

@ -21,11 +21,14 @@
"jotai": "^2.11.3",
"one": "1.1.426",
"react": "^19.0.0",
"react-i18next": "^15.4.0",
"react-native": "0.77.0",
"react-native-reanimated": "~3.16.7",
"react-native-safe-area-context": "5.1.0",
"react-native-screens": "4.6.0",
"react-native-web": "^0.19.13"
"react-native-web": "^0.19.13",
"yoshiki": "1.2.14",
"zod": "^3.24.1"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",

View File

@ -21,12 +21,6 @@
export * from "./accounts";
export { storage } from "./account-internal";
export * from "./theme";
export * from "./resources";
export * from "./traits";
export * from "./page";
export * from "./kyoo-errors";
export * from "./utils";
export * from "./login";
export * from "./issue";
export * from "./query";

View File

@ -1,119 +0,0 @@
/*
* 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 { type Page, type QueryIdentifier, useFetch } from "@kyoo/models";
import { type Breakpoint, P } from "@kyoo/primitives";
import { type ComponentType, type ReactElement, isValidElement } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { ErrorView } from "./errors";
export type Layout = {
numColumns: Breakpoint<number>;
size: Breakpoint<number>;
gap: Breakpoint<number>;
layout: "grid" | "horizontal" | "vertical";
};
export type WithLoading<Item> =
| (Item & { isLoading: false })
| (Partial<Item> & { isLoading: true });
const isPage = <T = unknown>(obj: unknown): obj is Page<T> =>
(typeof obj === "object" && obj && "items" in obj) || false;
export const Fetch = <Data,>({
query,
placeholderCount = 1,
children,
}: {
query: QueryIdentifier<Data>;
placeholderCount?: number;
children: (
item: Data extends Page<infer Item> ? WithLoading<Item> : WithLoading<Data>,
i: number,
) => JSX.Element | null;
}): JSX.Element | null => {
const { data, isPaused, error } = useFetch(query);
if (error) return <ErrorView error={error} />;
if (isPaused) return <OfflineView />;
if (!data) {
const placeholders = [...Array(placeholderCount)].map((_, i) =>
children({ isLoading: true } as any, i),
);
return <>{placeholderCount === 1 ? placeholders[0] : placeholders}</>;
}
if (!isPage<object>(data))
return children(data ? { ...data, isLoading: false } : ({ isLoading: true } as any), 0);
return <>{data.items.map((item, i) => children({ ...item, isLoading: false } as any, i))}</>;
};
export const OfflineView = () => {
const { css } = useYoshiki();
const { t } = useTranslation();
return (
<View
{...css({
flexGrow: 1,
flexShrink: 1,
justifyContent: "center",
alignItems: "center",
})}
>
<P {...css({ color: (theme) => theme.colors.white })}>{t("errors.offline")}</P>
</View>
);
};
export const EmptyView = ({ message }: { message: string }) => {
const { css } = useYoshiki();
return (
<View
{...css({
flexGrow: 1,
justifyContent: "center",
alignItems: "center",
})}
>
<P {...css({ color: (theme) => theme.heading })}>{message}</P>
</View>
);
};
export const addHeader = <Props,>(
Header: ComponentType<{ children: JSX.Element } & Props> | ReactElement | undefined,
children: ReactElement,
headerProps?: Props,
) => {
if (!Header) return children;
return !isValidElement(Header) ? (
// @ts-ignore
<Header {...(headerProps ?? {})}>{children}</Header>
) : (
<>
{Header}
{children}
</>
);
};

View File

@ -82,7 +82,7 @@ NewsList.query = (): QueryIdentifier<News> => ({
infinite: true,
path: ["news"],
params: {
// Limit the inital numbers of items
// Limit the initial numbers of items
limit: 10,
fields: ["show", "watchStatus"],
},

View File

@ -44,3 +44,6 @@ export const Paged = <Item>(item: z.ZodType<Item>): z.ZodSchema<Page<Item>> =>
count: z.number(),
items: z.array(item),
});
export const isPage = <T = unknown>(obj: unknown): obj is Page<T> =>
(typeof obj === "object" && obj && "items" in obj) || false;

58
front/src/query/fetch.tsx Normal file
View File

@ -0,0 +1,58 @@
import { type ComponentType, type ReactElement, isValidElement } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { P } from "~/primitives";
import { ErrorView } from "./errors";
import { type QueryIdentifier, useFetch } from "./query";
export const Fetch = <Data,>({
query,
Render,
Loader,
}: {
query: QueryIdentifier<Data>;
Render: (item: Data) => ReactElement;
Loader: () => ReactElement;
}): JSX.Element | null => {
const { data, isPaused, error } = useFetch(query);
if (error) return <ErrorView error={error} />;
if (isPaused) return <OfflineView />;
if (!data) return <Loader />;
return <Render {...data} />;
};
export const OfflineView = () => {
const { css } = useYoshiki();
const { t } = useTranslation();
return (
<View
{...css({
flexGrow: 1,
flexShrink: 1,
justifyContent: "center",
alignItems: "center",
})}
>
<P {...css({ color: (theme) => theme.colors.white })}>{t("errors.offline")}</P>
</View>
);
};
export const EmptyView = ({ message }: { message: string }) => {
const { css } = useYoshiki();
return (
<View
{...css({
flexGrow: 1,
justifyContent: "center",
alignItems: "center",
})}
>
<P {...css({ color: (theme) => theme.heading })}>{message}</P>
</View>
);
};

View File

@ -0,0 +1,2 @@
export * from "./query";
export * from "./fetch";