mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Rework Fetch component
This commit is contained in:
parent
930fb3edfc
commit
43e1dfd2cd
@ -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"],
|
||||
},
|
||||
});
|
||||
|
510
front/bun.lock
510
front/bun.lock
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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";
|
||||
|
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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"],
|
||||
},
|
||||
|
@ -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
58
front/src/query/fetch.tsx
Normal 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>
|
||||
);
|
||||
};
|
2
front/src/query/index.tsx
Normal file
2
front/src/query/index.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./query";
|
||||
export * from "./fetch";
|
Loading…
x
Reference in New Issue
Block a user