mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add a fetch component
This commit is contained in:
parent
9a9538d14d
commit
20c740ce07
@ -8,6 +8,7 @@ COPY apps/web/package.json apps/web/package.json
|
||||
COPY apps/mobile/package.json apps/mobile/package.json
|
||||
COPY packages/ui/package.json packages/ui/package.json
|
||||
COPY packages/primitives/package.json packages/primitives/package.json
|
||||
COPY packages/models/package.json packages/models/package.json
|
||||
RUN yarn --immutable
|
||||
|
||||
COPY . .
|
||||
|
@ -8,6 +8,7 @@ COPY apps/web/package.json apps/web/package.json
|
||||
COPY apps/mobile/package.json apps/mobile/package.json
|
||||
COPY packages/ui/package.json packages/ui/package.json
|
||||
COPY packages/primitives/package.json packages/primitives/package.json
|
||||
COPY packages/models/package.json packages/models/package.json
|
||||
RUN yarn --immutable
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
@ -1,4 +1,23 @@
|
||||
// Learn more https://docs.expo.dev/guides/monorepos
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
const path = require("path");
|
||||
|
||||
|
@ -81,6 +81,7 @@ const nextConfig = {
|
||||
transpilePackages: [
|
||||
"@kyoo/ui",
|
||||
"@kyoo/primitives",
|
||||
"@kyoo/models",
|
||||
"solito",
|
||||
"react-native",
|
||||
"react-native-web",
|
||||
|
@ -14,12 +14,12 @@
|
||||
"@emotion/react": "^11.9.3",
|
||||
"@emotion/styled": "^11.9.3",
|
||||
"@jellyfin/libass-wasm": "^4.1.1",
|
||||
"@kyoo/models": "workspace:^",
|
||||
"@kyoo/primitives": "workspace:^",
|
||||
"@kyoo/ui": "workspace:^",
|
||||
"@mui/icons-material": "^5.8.4",
|
||||
"@mui/material": "^5.8.7",
|
||||
"@mui/system": "^5.10.10",
|
||||
"@tanstack/react-query": "^4.18.0",
|
||||
"clsx": "^1.2.1",
|
||||
"csstype": "^3.1.1",
|
||||
"hls.js": "^1.2.8",
|
||||
|
21
front/apps/web/src/models.ts
Normal file
21
front/apps/web/src/models.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
export * from "@kyoo/models";
|
@ -39,7 +39,7 @@ import { ErrorPage } from "~/components/errors";
|
||||
import { Navbar } from "@kyoo/ui";
|
||||
import { Poster, Image } from "@kyoo/primitives";
|
||||
import { ItemType, LibraryItem, LibraryItemP } from "~/models";
|
||||
import { getDisplayDate } from "~/models/utils";
|
||||
import { getDisplayDate } from "@kyoo/models";
|
||||
import { InfiniteScroll } from "~/utils/infinite-scroll";
|
||||
import { Link } from "~/utils/link";
|
||||
import { withRoute } from "~/utils/router";
|
||||
|
@ -28,7 +28,7 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
import { z } from "zod";
|
||||
import { KyooErrors, Page } from "~/models";
|
||||
import { Paged } from "~/models/page";
|
||||
import { Paged } from "~/models";
|
||||
|
||||
const queryFn = async <Data>(
|
||||
type: z.ZodType<Data>,
|
||||
|
23
front/packages/models/package.json
Normal file
23
front/packages/models/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@kyoo/models",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"packageManager": "yarn@3.2.4",
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.25",
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-native-web": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^4.18.0",
|
||||
"zod": "^3.19.1"
|
||||
}
|
||||
}
|
@ -20,5 +20,8 @@
|
||||
|
||||
export * from "./page";
|
||||
export * from "./kyoo-errors";
|
||||
export * from "./utils"
|
||||
export * from "./traits";
|
||||
export * from "./resources";
|
||||
|
||||
export * from "./query";
|
207
front/packages/models/src/query.tsx
Normal file
207
front/packages/models/src/query.tsx
Normal file
@ -0,0 +1,207 @@
|
||||
/*
|
||||
* 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 { ComponentType, ReactElement, ReactNode } from "react";
|
||||
import {
|
||||
dehydrate,
|
||||
QueryClient,
|
||||
QueryFunctionContext,
|
||||
useInfiniteQuery,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { z } from "zod";
|
||||
import { KyooErrors } from "./kyoo-errors";
|
||||
import { Page, Paged } from "./page";
|
||||
import { Library } from "./resources";
|
||||
|
||||
const queryFn = async <Data,>(
|
||||
type: z.ZodType<Data>,
|
||||
context: QueryFunctionContext,
|
||||
): Promise<Data> => {
|
||||
const kyoo_url = process.env.KYOO_URL ?? "http://localhost:5000";
|
||||
|
||||
let resp;
|
||||
try {
|
||||
resp = await fetch(
|
||||
[typeof window === "undefined" ? kyoo_url : "/api"]
|
||||
.concat(
|
||||
context.pageParam ? [context.pageParam] : (context.queryKey.filter((x) => x) as string[]),
|
||||
)
|
||||
.join("/")
|
||||
.replace("/?", "?"),
|
||||
);
|
||||
} catch (e) {
|
||||
console.log("Fetch error", e);
|
||||
throw { errors: ["Could not reach Kyoo's server."] } as KyooErrors;
|
||||
}
|
||||
if (resp.status === 404) {
|
||||
throw { errors: ["Resource not found."] } as KyooErrors;
|
||||
}
|
||||
if (!resp.ok) {
|
||||
const error = await resp.text();
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(error);
|
||||
} catch (e) {
|
||||
data = { errors: [error] } as KyooErrors;
|
||||
}
|
||||
console.log("Invalid response:", data);
|
||||
throw data as KyooErrors;
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await resp.json();
|
||||
} catch (e) {
|
||||
console.error("Invald json from kyoo", e);
|
||||
throw { errors: ["Invalid repsonse from kyoo"] };
|
||||
}
|
||||
const parsed = await type.safeParseAsync(data);
|
||||
if (!parsed.success) {
|
||||
console.log("Parse error: ", parsed.error);
|
||||
throw { errors: parsed.error.errors.map((x) => x.message) } as KyooErrors;
|
||||
}
|
||||
return parsed.data;
|
||||
};
|
||||
|
||||
export const createQueryClient = () =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: Infinity,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type QueryIdentifier<T = unknown> = {
|
||||
parser: z.ZodType<T, z.ZodTypeDef, any>;
|
||||
path: (string | undefined)[];
|
||||
params?: { [query: string]: boolean | number | string | string[] | undefined };
|
||||
infinite?: boolean;
|
||||
};
|
||||
|
||||
export type QueryPage<Props = {}> = ComponentType<Props> & {
|
||||
getFetchUrls?: (route: { [key: string]: string }) => QueryIdentifier[];
|
||||
getLayout?: (page: ReactElement) => ReactNode;
|
||||
};
|
||||
|
||||
const toQueryKey = <Data,>(query: QueryIdentifier<Data>) => {
|
||||
if (query.params) {
|
||||
return [
|
||||
...query.path,
|
||||
"?" +
|
||||
Object.entries(query.params)
|
||||
.filter(([_, v]) => v !== undefined)
|
||||
.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`)
|
||||
.join("&"),
|
||||
];
|
||||
} else {
|
||||
return query.path;
|
||||
}
|
||||
};
|
||||
|
||||
export const useFetch = <Data,>(query: QueryIdentifier<Data>) => {
|
||||
return useQuery<Data, KyooErrors>({
|
||||
queryKey: toQueryKey(query),
|
||||
queryFn: (ctx) => queryFn(query.parser, ctx),
|
||||
});
|
||||
};
|
||||
|
||||
export const useInfiniteFetch = <Data,>(query: QueryIdentifier<Data>) => {
|
||||
const ret = useInfiniteQuery<Page<Data>, KyooErrors>({
|
||||
queryKey: toQueryKey(query),
|
||||
queryFn: (ctx) => queryFn(Paged(query.parser), ctx),
|
||||
getNextPageParam: (page: Page<Data>) => page?.next || undefined,
|
||||
});
|
||||
return { ...ret, items: ret.data?.pages.flatMap((x) => x.items) };
|
||||
};
|
||||
|
||||
export const fetchQuery = async (queries: QueryIdentifier[]) => {
|
||||
// we can't put this check in a function because we want build time optimizations
|
||||
// see https://github.com/vercel/next.js/issues/5354 for details
|
||||
if (typeof window !== "undefined") return {};
|
||||
|
||||
const client = createQueryClient();
|
||||
await Promise.all(
|
||||
queries.map((query) => {
|
||||
if (query.infinite) {
|
||||
return client.prefetchInfiniteQuery({
|
||||
queryKey: toQueryKey(query),
|
||||
queryFn: (ctx) => queryFn(Paged(query.parser), ctx),
|
||||
});
|
||||
} else {
|
||||
return client.prefetchQuery({
|
||||
queryKey: toQueryKey(query),
|
||||
queryFn: (ctx) => queryFn(query.parser, ctx),
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
return dehydrate(client);
|
||||
};
|
||||
|
||||
/* export const Fetch = <Data,>({ */
|
||||
/* query, */
|
||||
/* children, */
|
||||
/* }: { */
|
||||
/* query: QueryIdentifier<Data>; */
|
||||
/* children: ( */
|
||||
/* item: (Data & { isLoading: false }) | { isLoading: true }, */
|
||||
/* i: number, */
|
||||
/* ) => JSX.Element | null; */
|
||||
/* }) => { */
|
||||
/* const { data, error, isSuccess, isError } = useFetch(query); */
|
||||
|
||||
/* return children(isSuccess ? { ...data, isLoading: false } : { isLoading: true }, 0); */
|
||||
/* }; */
|
||||
|
||||
type WithLoading<Item> = (Item & { isLoading: false }) | { 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,
|
||||
children,
|
||||
}: {
|
||||
query: QueryIdentifier<Data>;
|
||||
placeholderCount?: number;
|
||||
children: (
|
||||
item: Data extends Page<infer Item> ? WithLoading<Item> : WithLoading<Data>,
|
||||
i: number,
|
||||
) => JSX.Element | null;
|
||||
}) => {
|
||||
const { data, error } = useFetch(query);
|
||||
|
||||
if (error) throw error;
|
||||
if (!isPage<object>(data))
|
||||
return <> {children(data ? { ...data, isLoading: false } : ({ isLoading: true } as any), 0)}</>;
|
||||
return (
|
||||
<>
|
||||
{data
|
||||
? data.items.map((item, i) => children({ ...item, isLoading: false } as any, i))
|
||||
: [...Array(placeholderCount)].map((_, i) => children({ isLoading: true } as any, i))}
|
||||
</>
|
||||
);
|
||||
};
|
26
front/packages/models/tsconfig.json
Executable file
26
front/packages/models/tsconfig.json
Executable file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"noEmit": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-native",
|
||||
"incremental": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
@ -20,6 +20,7 @@
|
||||
"dependencies": {
|
||||
"@expo/html-elements": "^0.2.2",
|
||||
"@expo/vector-icons": "AnonymusRaccoon/expo-vector-icons#no-prepare",
|
||||
"@tanstack/react-query": "^4.18.0",
|
||||
"solito": "^2.0.5"
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
"types": "src/index.ts",
|
||||
"packageManager": "yarn@3.2.4",
|
||||
"dependencies": {
|
||||
"@kyoo/models": "workspace:^",
|
||||
"@kyoo/primitives": "workspace:^",
|
||||
"react-native-svg": "^13.6.0"
|
||||
},
|
||||
|
@ -19,12 +19,10 @@
|
||||
*/
|
||||
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
/* import { Library, LibraryP, Page, Paged } from "~/models"; */
|
||||
/* import { QueryIdentifier, useFetch } from "~/utils/query"; */
|
||||
/* import { ErrorSnackbar } from "./errors"; */
|
||||
import { Library, LibraryP, Page, Paged, Fetch, QueryIdentifier } from "@kyoo/models";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { IconButton, Header, Avatar, A, ts } from "@kyoo/primitives";
|
||||
import { View } from "react-native";
|
||||
import { Text, View } from "react-native";
|
||||
import { KyooLongLogo } from "./icon";
|
||||
|
||||
const tooltip = (tooltip: string): object => ({});
|
||||
@ -34,7 +32,6 @@ export const NavbarTitle = KyooLongLogo;
|
||||
export const Navbar = () => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation("common");
|
||||
/* const { data, error, isSuccess, isError } = useFetch(Navbar.query()); */
|
||||
|
||||
return (
|
||||
<Header
|
||||
@ -72,30 +69,30 @@ export const Navbar = () => {
|
||||
marginLeft: ts(2),
|
||||
})}
|
||||
>
|
||||
{
|
||||
/*isSuccess
|
||||
? data.items.map((library) => */ true
|
||||
? [...Array(4)].map((_, i) => (
|
||||
<A
|
||||
href={`/browse/${i /* library.slug */}`}
|
||||
key={i} //{library.slug}
|
||||
{...css({
|
||||
marginX: ts(1),
|
||||
textTransform: "uppercase",
|
||||
color: "white",
|
||||
})}
|
||||
>
|
||||
Toto
|
||||
{/* {library.name} */}
|
||||
</A>
|
||||
))
|
||||
: [...Array(4)].map(
|
||||
(_, i) => null,
|
||||
/* <Typography key={i} variant="button" px=".25rem"> */
|
||||
/* <Skeleton width="5rem" /> */
|
||||
/* </Typography> */
|
||||
)
|
||||
}
|
||||
<Fetch query={Navbar.query()}>
|
||||
{(library, i) =>
|
||||
!library.isLoading ? (
|
||||
<A
|
||||
href={`/browse/${library.slug}`}
|
||||
key={library.slug}
|
||||
{...css({
|
||||
marginX: ts(1),
|
||||
textTransform: "uppercase",
|
||||
color: "white",
|
||||
})}
|
||||
>
|
||||
{library.name}
|
||||
</A>
|
||||
) : (
|
||||
<>
|
||||
<Text>Toto</Text>
|
||||
{/* <Typography key={i} variant="button" px=".25rem"> */}
|
||||
{/* <Skeleton width="5rem" /> */}
|
||||
{/* </Typography> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Fetch>
|
||||
</View>
|
||||
<A href="/auth/login" {...tooltip(t("navbar.login"))}>
|
||||
<Avatar alt={t("navbar.login")} size={30} />
|
||||
@ -105,7 +102,7 @@ export const Navbar = () => {
|
||||
);
|
||||
};
|
||||
|
||||
/* Navbar.query = (): QueryIdentifier<Page<Library>> => ({ */
|
||||
/* parser: Paged(LibraryP), */
|
||||
/* path: ["libraries"], */
|
||||
/* }); */
|
||||
Navbar.query = (): QueryIdentifier<Page<Library>> => ({
|
||||
parser: Paged(LibraryP),
|
||||
path: ["libraries"],
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user