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 { Text, View } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/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();
|
const { css } = useYoshiki();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<Fetch
|
||||||
{...css({
|
query={NewsList.query()}
|
||||||
flex: 1,
|
layout={{ ...ItemGrid.layout, layout: "horizontal" }}
|
||||||
justifyContent: "center",
|
getItemType={(x, i) => (x?.kind === "movie" || (!x && i % 2) ? "movie" : "episode")}
|
||||||
alignItems: "center",
|
getItemSize={(kind) => (kind === "episode" ? 2 : 1)}
|
||||||
minHeight: "100%",
|
empty={t("home.none")}
|
||||||
})}
|
Render={({ item }) => {
|
||||||
>
|
<Text>{item.name}</Text>;
|
||||||
<Text>Hello from One</Text>
|
}}
|
||||||
</View>
|
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",
|
"jotai": "^2.11.3",
|
||||||
"one": "1.1.426",
|
"one": "1.1.426",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
"react-i18next": "^15.4.0",
|
||||||
"react-native": "0.77.0",
|
"react-native": "0.77.0",
|
||||||
"react-native-reanimated": "~3.16.7",
|
"react-native-reanimated": "~3.16.7",
|
||||||
"react-native-safe-area-context": "5.1.0",
|
"react-native-safe-area-context": "5.1.0",
|
||||||
"react-native-screens": "4.6.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": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
|
@ -21,12 +21,6 @@
|
|||||||
export * from "./accounts";
|
export * from "./accounts";
|
||||||
export { storage } from "./account-internal";
|
export { storage } from "./account-internal";
|
||||||
export * from "./theme";
|
export * from "./theme";
|
||||||
export * from "./resources";
|
|
||||||
export * from "./traits";
|
|
||||||
export * from "./page";
|
|
||||||
export * from "./kyoo-errors";
|
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
export * from "./login";
|
export * from "./login";
|
||||||
export * from "./issue";
|
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,
|
infinite: true,
|
||||||
path: ["news"],
|
path: ["news"],
|
||||||
params: {
|
params: {
|
||||||
// Limit the inital numbers of items
|
// Limit the initial numbers of items
|
||||||
limit: 10,
|
limit: 10,
|
||||||
fields: ["show", "watchStatus"],
|
fields: ["show", "watchStatus"],
|
||||||
},
|
},
|
||||||
|
@ -44,3 +44,6 @@ export const Paged = <Item>(item: z.ZodType<Item>): z.ZodSchema<Page<Item>> =>
|
|||||||
count: z.number(),
|
count: z.number(),
|
||||||
items: z.array(item),
|
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