From 20c740ce07f38f19341ef29bf8706d1f1778bfdf Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 3 Dec 2022 11:33:58 +0900 Subject: [PATCH 1/3] Add a fetch component --- front/Dockerfile | 1 + front/Dockerfile.dev | 1 + front/apps/mobile/metro.config.js | 21 +- front/apps/web/next.config.js | 1 + front/apps/web/package.json | 2 +- front/apps/web/src/models.ts | 21 ++ front/apps/web/src/pages/browse/index.tsx | 2 +- front/apps/web/src/utils/query.ts | 2 +- front/packages/models/package.json | 23 ++ .../models => packages/models/src}/index.ts | 3 + .../models/src}/kyoo-errors.ts | 0 .../models => packages/models/src}/page.ts | 0 front/packages/models/src/query.tsx | 207 ++++++++++++++++++ .../models/src}/resources/collection.ts | 0 .../models/src}/resources/episode.ts | 0 .../models/src}/resources/genre.ts | 0 .../models/src}/resources/index.ts | 0 .../models/src}/resources/library-item.ts | 0 .../models/src}/resources/library.ts | 0 .../models/src}/resources/movie.ts | 0 .../models/src}/resources/person.ts | 0 .../models/src}/resources/season.ts | 0 .../models/src}/resources/show.ts | 0 .../models/src}/resources/studio.ts | 0 .../models/src}/resources/watch-item.ts | 0 .../models/src}/traits/images.ts | 0 .../models/src}/traits/index.ts | 0 .../models/src}/traits/resource.ts | 0 .../models => packages/models/src}/utils.ts | 0 front/packages/models/tsconfig.json | 26 +++ front/packages/primitives/package.json | 1 + front/packages/ui/package.json | 1 + front/packages/ui/src/navbar/index.tsx | 63 +++--- 33 files changed, 338 insertions(+), 37 deletions(-) create mode 100644 front/apps/web/src/models.ts create mode 100644 front/packages/models/package.json rename front/{apps/web/src/models => packages/models/src}/index.ts (94%) rename front/{apps/web/src/models => packages/models/src}/kyoo-errors.ts (100%) rename front/{apps/web/src/models => packages/models/src}/page.ts (100%) create mode 100644 front/packages/models/src/query.tsx rename front/{apps/web/src/models => packages/models/src}/resources/collection.ts (100%) rename front/{apps/web/src/models => packages/models/src}/resources/episode.ts (100%) rename front/{apps/web/src/models => packages/models/src}/resources/genre.ts (100%) rename front/{apps/web/src/models => packages/models/src}/resources/index.ts (100%) rename front/{apps/web/src/models => packages/models/src}/resources/library-item.ts (100%) rename front/{apps/web/src/models => packages/models/src}/resources/library.ts (100%) rename front/{apps/web/src/models => packages/models/src}/resources/movie.ts (100%) rename front/{apps/web/src/models => packages/models/src}/resources/person.ts (100%) rename front/{apps/web/src/models => packages/models/src}/resources/season.ts (100%) rename front/{apps/web/src/models => packages/models/src}/resources/show.ts (100%) rename front/{apps/web/src/models => packages/models/src}/resources/studio.ts (100%) rename front/{apps/web/src/models => packages/models/src}/resources/watch-item.ts (100%) rename front/{apps/web/src/models => packages/models/src}/traits/images.ts (100%) rename front/{apps/web/src/models => packages/models/src}/traits/index.ts (100%) rename front/{apps/web/src/models => packages/models/src}/traits/resource.ts (100%) rename front/{apps/web/src/models => packages/models/src}/utils.ts (100%) create mode 100755 front/packages/models/tsconfig.json diff --git a/front/Dockerfile b/front/Dockerfile index c341b8ce..6ff38b3a 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -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 . . diff --git a/front/Dockerfile.dev b/front/Dockerfile.dev index 25657fef..b1e58e67 100644 --- a/front/Dockerfile.dev +++ b/front/Dockerfile.dev @@ -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 diff --git a/front/apps/mobile/metro.config.js b/front/apps/mobile/metro.config.js index 36560b59..33d774e3 100644 --- a/front/apps/mobile/metro.config.js +++ b/front/apps/mobile/metro.config.js @@ -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 . + */ + const { getDefaultConfig } = require("expo/metro-config"); const path = require("path"); diff --git a/front/apps/web/next.config.js b/front/apps/web/next.config.js index 320870e3..be7877a8 100755 --- a/front/apps/web/next.config.js +++ b/front/apps/web/next.config.js @@ -81,6 +81,7 @@ const nextConfig = { transpilePackages: [ "@kyoo/ui", "@kyoo/primitives", + "@kyoo/models", "solito", "react-native", "react-native-web", diff --git a/front/apps/web/package.json b/front/apps/web/package.json index 61086978..dcd0bfae 100644 --- a/front/apps/web/package.json +++ b/front/apps/web/package.json @@ -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", diff --git a/front/apps/web/src/models.ts b/front/apps/web/src/models.ts new file mode 100644 index 00000000..e451d54b --- /dev/null +++ b/front/apps/web/src/models.ts @@ -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 . + */ + +export * from "@kyoo/models"; diff --git a/front/apps/web/src/pages/browse/index.tsx b/front/apps/web/src/pages/browse/index.tsx index b6d62b0e..85c20a2c 100644 --- a/front/apps/web/src/pages/browse/index.tsx +++ b/front/apps/web/src/pages/browse/index.tsx @@ -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"; diff --git a/front/apps/web/src/utils/query.ts b/front/apps/web/src/utils/query.ts index 2808d832..806c6d89 100644 --- a/front/apps/web/src/utils/query.ts +++ b/front/apps/web/src/utils/query.ts @@ -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 ( type: z.ZodType, diff --git a/front/packages/models/package.json b/front/packages/models/package.json new file mode 100644 index 00000000..c0adccba --- /dev/null +++ b/front/packages/models/package.json @@ -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" + } +} diff --git a/front/apps/web/src/models/index.ts b/front/packages/models/src/index.ts similarity index 94% rename from front/apps/web/src/models/index.ts rename to front/packages/models/src/index.ts index 4932e1fc..025a2157 100644 --- a/front/apps/web/src/models/index.ts +++ b/front/packages/models/src/index.ts @@ -20,5 +20,8 @@ export * from "./page"; export * from "./kyoo-errors"; +export * from "./utils" export * from "./traits"; export * from "./resources"; + +export * from "./query"; diff --git a/front/apps/web/src/models/kyoo-errors.ts b/front/packages/models/src/kyoo-errors.ts similarity index 100% rename from front/apps/web/src/models/kyoo-errors.ts rename to front/packages/models/src/kyoo-errors.ts diff --git a/front/apps/web/src/models/page.ts b/front/packages/models/src/page.ts similarity index 100% rename from front/apps/web/src/models/page.ts rename to front/packages/models/src/page.ts diff --git a/front/packages/models/src/query.tsx b/front/packages/models/src/query.tsx new file mode 100644 index 00000000..74b58d75 --- /dev/null +++ b/front/packages/models/src/query.tsx @@ -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 . + */ + +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 ( + type: z.ZodType, + context: QueryFunctionContext, +): Promise => { + 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 = { + parser: z.ZodType; + path: (string | undefined)[]; + params?: { [query: string]: boolean | number | string | string[] | undefined }; + infinite?: boolean; +}; + +export type QueryPage = ComponentType & { + getFetchUrls?: (route: { [key: string]: string }) => QueryIdentifier[]; + getLayout?: (page: ReactElement) => ReactNode; +}; + +const toQueryKey = (query: QueryIdentifier) => { + 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 = (query: QueryIdentifier) => { + return useQuery({ + queryKey: toQueryKey(query), + queryFn: (ctx) => queryFn(query.parser, ctx), + }); +}; + +export const useInfiniteFetch = (query: QueryIdentifier) => { + const ret = useInfiniteQuery, KyooErrors>({ + queryKey: toQueryKey(query), + queryFn: (ctx) => queryFn(Paged(query.parser), ctx), + getNextPageParam: (page: Page) => 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 = ({ */ +/* query, */ +/* children, */ +/* }: { */ +/* query: QueryIdentifier; */ +/* 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 & { isLoading: false }) | { isLoading: true }; + +const isPage = (obj: unknown): obj is Page => + (typeof obj === "object" && obj && "items" in obj) || false; + +export const Fetch = ({ + query, + placeholderCount, + children, +}: { + query: QueryIdentifier; + placeholderCount?: number; + children: ( + item: Data extends Page ? WithLoading : WithLoading, + i: number, + ) => JSX.Element | null; +}) => { + const { data, error } = useFetch(query); + + if (error) throw error; + if (!isPage(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))} + + ); +}; diff --git a/front/apps/web/src/models/resources/collection.ts b/front/packages/models/src/resources/collection.ts similarity index 100% rename from front/apps/web/src/models/resources/collection.ts rename to front/packages/models/src/resources/collection.ts diff --git a/front/apps/web/src/models/resources/episode.ts b/front/packages/models/src/resources/episode.ts similarity index 100% rename from front/apps/web/src/models/resources/episode.ts rename to front/packages/models/src/resources/episode.ts diff --git a/front/apps/web/src/models/resources/genre.ts b/front/packages/models/src/resources/genre.ts similarity index 100% rename from front/apps/web/src/models/resources/genre.ts rename to front/packages/models/src/resources/genre.ts diff --git a/front/apps/web/src/models/resources/index.ts b/front/packages/models/src/resources/index.ts similarity index 100% rename from front/apps/web/src/models/resources/index.ts rename to front/packages/models/src/resources/index.ts diff --git a/front/apps/web/src/models/resources/library-item.ts b/front/packages/models/src/resources/library-item.ts similarity index 100% rename from front/apps/web/src/models/resources/library-item.ts rename to front/packages/models/src/resources/library-item.ts diff --git a/front/apps/web/src/models/resources/library.ts b/front/packages/models/src/resources/library.ts similarity index 100% rename from front/apps/web/src/models/resources/library.ts rename to front/packages/models/src/resources/library.ts diff --git a/front/apps/web/src/models/resources/movie.ts b/front/packages/models/src/resources/movie.ts similarity index 100% rename from front/apps/web/src/models/resources/movie.ts rename to front/packages/models/src/resources/movie.ts diff --git a/front/apps/web/src/models/resources/person.ts b/front/packages/models/src/resources/person.ts similarity index 100% rename from front/apps/web/src/models/resources/person.ts rename to front/packages/models/src/resources/person.ts diff --git a/front/apps/web/src/models/resources/season.ts b/front/packages/models/src/resources/season.ts similarity index 100% rename from front/apps/web/src/models/resources/season.ts rename to front/packages/models/src/resources/season.ts diff --git a/front/apps/web/src/models/resources/show.ts b/front/packages/models/src/resources/show.ts similarity index 100% rename from front/apps/web/src/models/resources/show.ts rename to front/packages/models/src/resources/show.ts diff --git a/front/apps/web/src/models/resources/studio.ts b/front/packages/models/src/resources/studio.ts similarity index 100% rename from front/apps/web/src/models/resources/studio.ts rename to front/packages/models/src/resources/studio.ts diff --git a/front/apps/web/src/models/resources/watch-item.ts b/front/packages/models/src/resources/watch-item.ts similarity index 100% rename from front/apps/web/src/models/resources/watch-item.ts rename to front/packages/models/src/resources/watch-item.ts diff --git a/front/apps/web/src/models/traits/images.ts b/front/packages/models/src/traits/images.ts similarity index 100% rename from front/apps/web/src/models/traits/images.ts rename to front/packages/models/src/traits/images.ts diff --git a/front/apps/web/src/models/traits/index.ts b/front/packages/models/src/traits/index.ts similarity index 100% rename from front/apps/web/src/models/traits/index.ts rename to front/packages/models/src/traits/index.ts diff --git a/front/apps/web/src/models/traits/resource.ts b/front/packages/models/src/traits/resource.ts similarity index 100% rename from front/apps/web/src/models/traits/resource.ts rename to front/packages/models/src/traits/resource.ts diff --git a/front/apps/web/src/models/utils.ts b/front/packages/models/src/utils.ts similarity index 100% rename from front/apps/web/src/models/utils.ts rename to front/packages/models/src/utils.ts diff --git a/front/packages/models/tsconfig.json b/front/packages/models/tsconfig.json new file mode 100755 index 00000000..32ca157b --- /dev/null +++ b/front/packages/models/tsconfig.json @@ -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"] +} diff --git a/front/packages/primitives/package.json b/front/packages/primitives/package.json index c88e5157..294ed35b 100644 --- a/front/packages/primitives/package.json +++ b/front/packages/primitives/package.json @@ -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" } } diff --git a/front/packages/ui/package.json b/front/packages/ui/package.json index 622abbae..71680ecc 100644 --- a/front/packages/ui/package.json +++ b/front/packages/ui/package.json @@ -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" }, diff --git a/front/packages/ui/src/navbar/index.tsx b/front/packages/ui/src/navbar/index.tsx index c3e64330..4f047e35 100644 --- a/front/packages/ui/src/navbar/index.tsx +++ b/front/packages/ui/src/navbar/index.tsx @@ -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 (
{ marginLeft: ts(2), })} > - { - /*isSuccess - ? data.items.map((library) => */ true - ? [...Array(4)].map((_, i) => ( - - Toto - {/* {library.name} */} - - )) - : [...Array(4)].map( - (_, i) => null, - /* */ - /* */ - /* */ - ) - } + + {(library, i) => + !library.isLoading ? ( + + {library.name} + + ) : ( + <> + Toto + {/* */} + {/* */} + {/* */} + + ) + } + @@ -105,7 +102,7 @@ export const Navbar = () => { ); }; -/* Navbar.query = (): QueryIdentifier> => ({ */ -/* parser: Paged(LibraryP), */ -/* path: ["libraries"], */ -/* }); */ +Navbar.query = (): QueryIdentifier> => ({ + parser: Paged(LibraryP), + path: ["libraries"], +}); From e82e515a234b1fc913320b9c289ff370b2d49bc2 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 3 Dec 2022 19:15:15 +0900 Subject: [PATCH 2/3] Fix zod utils import --- front/apps/web/src/utils/zod.ts | 33 ------------------- .../packages/models/src/resources/episode.ts | 2 +- front/packages/models/src/resources/movie.ts | 2 +- front/packages/models/src/resources/season.ts | 2 +- front/packages/models/src/resources/show.ts | 2 +- .../models/src/resources/watch-item.ts | 2 +- front/packages/models/src/utils.ts | 13 ++++++++ 7 files changed, 18 insertions(+), 38 deletions(-) delete mode 100644 front/apps/web/src/utils/zod.ts diff --git a/front/apps/web/src/utils/zod.ts b/front/apps/web/src/utils/zod.ts deleted file mode 100644 index ebcca5df..00000000 --- a/front/apps/web/src/utils/zod.ts +++ /dev/null @@ -1,33 +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 . - */ - -import { z } from "zod"; - -export const zdate = () => { - return z.preprocess((arg) => { - if (arg instanceof Date) return arg; - - if (typeof arg === "string" && /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?/.test(arg)) { - return new Date(arg); - } - - return undefined; - }, z.date()); -}; diff --git a/front/packages/models/src/resources/episode.ts b/front/packages/models/src/resources/episode.ts index 27c91985..8df707ba 100644 --- a/front/packages/models/src/resources/episode.ts +++ b/front/packages/models/src/resources/episode.ts @@ -19,7 +19,7 @@ */ import { z } from "zod"; -import { zdate } from "~/utils/zod"; +import { zdate } from "../utils"; import { ImagesP } from "../traits"; import { ResourceP } from "../traits/resource"; diff --git a/front/packages/models/src/resources/movie.ts b/front/packages/models/src/resources/movie.ts index 44fa0305..ef2f7802 100644 --- a/front/packages/models/src/resources/movie.ts +++ b/front/packages/models/src/resources/movie.ts @@ -19,7 +19,7 @@ */ import { z } from "zod"; -import { zdate } from "~/utils/zod"; +import { zdate } from "../utils"; import { ImagesP, ResourceP } from "../traits"; import { GenreP } from "./genre"; import { StudioP } from "./studio"; diff --git a/front/packages/models/src/resources/season.ts b/front/packages/models/src/resources/season.ts index 75e357f5..e858f7eb 100644 --- a/front/packages/models/src/resources/season.ts +++ b/front/packages/models/src/resources/season.ts @@ -19,7 +19,7 @@ */ import { z } from "zod"; -import { zdate } from "~/utils/zod"; +import { zdate } from "../utils"; import { ImagesP } from "../traits"; import { ResourceP } from "../traits/resource"; diff --git a/front/packages/models/src/resources/show.ts b/front/packages/models/src/resources/show.ts index 4a6cf6c3..9e61370a 100644 --- a/front/packages/models/src/resources/show.ts +++ b/front/packages/models/src/resources/show.ts @@ -19,7 +19,7 @@ */ import { z } from "zod"; -import { zdate } from "~/utils/zod"; +import { zdate } from "../utils"; import { ImagesP, ResourceP } from "../traits"; import { GenreP } from "./genre"; import { SeasonP } from "./season"; diff --git a/front/packages/models/src/resources/watch-item.ts b/front/packages/models/src/resources/watch-item.ts index 47f4ed8c..620530d4 100644 --- a/front/packages/models/src/resources/watch-item.ts +++ b/front/packages/models/src/resources/watch-item.ts @@ -19,7 +19,7 @@ */ import { z } from "zod"; -import { zdate } from "~/utils/zod"; +import { zdate } from "../utils"; import { ResourceP, ImagesP, imageFn } from "../traits"; import { EpisodeP } from "./episode"; diff --git a/front/packages/models/src/utils.ts b/front/packages/models/src/utils.ts index 8336c64f..0954563a 100644 --- a/front/packages/models/src/utils.ts +++ b/front/packages/models/src/utils.ts @@ -19,6 +19,19 @@ */ import { Movie, Show } from "./resources"; +import { z } from "zod"; + +export const zdate = () => { + return z.preprocess((arg) => { + if (arg instanceof Date) return arg; + + if (typeof arg === "string" && /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?/.test(arg)) { + return new Date(arg); + } + + return undefined; + }, z.date()); +}; export const getDisplayDate = (data: Show | Movie) => { const { From 67de27578e978edf7dd233494d9a8d345937c6c8 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 3 Dec 2022 22:34:50 +0900 Subject: [PATCH 3/3] Add error handling in the fetch component --- front/apps/mobile/app.json | 2 +- front/apps/mobile/app/_layout.tsx | 13 +- front/apps/mobile/app/index.tsx | 3 +- front/apps/web/src/pages/_app.tsx | 2 +- front/apps/web/src/pages/browse/index.tsx | 2 +- front/apps/web/src/utils/query.ts | 160 ------------------ front/packages/models/src/index.ts | 4 +- front/packages/models/src/query.tsx | 47 ----- .../models/src/resources/library-item.ts | 2 +- front/packages/models/tsconfig.json | 2 +- .../primitives/src/themes/catppuccin.ts | 19 +++ .../packages/primitives/src/themes/theme.tsx | 8 + front/packages/primitives/tsconfig.json | 2 +- front/packages/ui/src/fetch.tsx | 73 ++++++++ front/packages/ui/src/navbar/index.tsx | 10 +- front/packages/ui/tsconfig.json | 2 - front/yarn.lock | 21 ++- 17 files changed, 146 insertions(+), 226 deletions(-) delete mode 100644 front/apps/web/src/utils/query.ts create mode 100644 front/packages/ui/src/fetch.tsx diff --git a/front/apps/mobile/app.json b/front/apps/mobile/app.json index 6d19ed5f..bb736b11 100644 --- a/front/apps/mobile/app.json +++ b/front/apps/mobile/app.json @@ -4,7 +4,7 @@ "slug": "kyoo", "scheme": "kyoo", "version": "1.0.0", - "orientation": "portrait", + "orientation": "default", "icon": "./assets/icon.png", "entryPoint": "./index.ts", "userInterfaceStyle": "light", diff --git a/front/apps/mobile/app/_layout.tsx b/front/apps/mobile/app/_layout.tsx index 3c96d1a1..98e9b1ce 100644 --- a/front/apps/mobile/app/_layout.tsx +++ b/front/apps/mobile/app/_layout.tsx @@ -22,6 +22,9 @@ import { Stack } from "expo-router"; import { Avatar, ThemeSelector } from "@kyoo/primitives"; import { useTheme } from "yoshiki/native"; import { NavbarTitle } from "@kyoo/ui"; +import { useState } from "react"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { createQueryClient } from "@kyoo/models"; const ThemedStack = () => { const theme = useTheme(); @@ -44,9 +47,13 @@ const ThemedStack = () => { }; export default function Root() { + const [queryClient] = useState(() => createQueryClient()); + return ( - - - + + + + + ); } diff --git a/front/apps/mobile/app/index.tsx b/front/apps/mobile/app/index.tsx index 47d23c48..9447d5b2 100644 --- a/front/apps/mobile/app/index.tsx +++ b/front/apps/mobile/app/index.tsx @@ -18,6 +18,7 @@ * along with Kyoo. If not, see . */ +import { Navbar } from "@kyoo/ui"; import { Text, View } from "react-native"; import { useYoshiki } from "yoshiki/native"; @@ -26,7 +27,7 @@ const App = () => { return ( theme.background })}> - {/* */} + toto ); diff --git a/front/apps/web/src/pages/_app.tsx b/front/apps/web/src/pages/_app.tsx index 4d394d7d..73e9e078 100755 --- a/front/apps/web/src/pages/_app.tsx +++ b/front/apps/web/src/pages/_app.tsx @@ -24,7 +24,7 @@ import { useTheme, useMobileHover } from "yoshiki/web"; import { createTheme, ThemeProvider as MTheme } from "@mui/material"; import NextApp, { AppContext, type AppProps } from "next/app"; import { Hydrate, QueryClientProvider } from "@tanstack/react-query"; -import { createQueryClient, fetchQuery, QueryIdentifier, QueryPage } from "~/utils/query"; +import { createQueryClient, fetchQuery, QueryIdentifier, QueryPage } from "@kyoo/models"; import superjson from "superjson"; import Head from "next/head"; import { ThemeSelector as KThemeSelector } from "@kyoo/primitives"; diff --git a/front/apps/web/src/pages/browse/index.tsx b/front/apps/web/src/pages/browse/index.tsx index 85c20a2c..9178c46d 100644 --- a/front/apps/web/src/pages/browse/index.tsx +++ b/front/apps/web/src/pages/browse/index.tsx @@ -43,7 +43,7 @@ import { getDisplayDate } from "@kyoo/models"; import { InfiniteScroll } from "~/utils/infinite-scroll"; import { Link } from "~/utils/link"; import { withRoute } from "~/utils/router"; -import { QueryIdentifier, QueryPage, useInfiniteFetch } from "~/utils/query"; +import { QueryIdentifier, QueryPage, useInfiniteFetch } from "@kyoo/models"; import { px } from "yoshiki/native"; enum SortBy { diff --git a/front/apps/web/src/utils/query.ts b/front/apps/web/src/utils/query.ts deleted file mode 100644 index 806c6d89..00000000 --- a/front/apps/web/src/utils/query.ts +++ /dev/null @@ -1,160 +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 . - */ - -import { ComponentType, ReactElement, ReactNode } from "react"; -import { - dehydrate, - QueryClient, - QueryFunctionContext, - useInfiniteQuery, - useQuery, -} from "@tanstack/react-query"; -import { z } from "zod"; -import { KyooErrors, Page } from "~/models"; -import { Paged } from "~/models"; - -const queryFn = async ( - type: z.ZodType, - context: QueryFunctionContext, -): Promise => { - 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 = { - parser: z.ZodType; - path: (string | undefined)[]; - params?: { [query: string]: boolean | number | string | string[] | undefined }; - infinite?: boolean; -}; - -export type QueryPage = ComponentType & { - getFetchUrls?: (route: { [key: string]: string }) => QueryIdentifier[]; - getLayout?: (page: ReactElement) => ReactNode; -}; - -const toQueryKey = (query: QueryIdentifier) => { - if (query.params) { - return [ - ...query.path, - "?" + - Object.entries(query.params) - .filter(([k, v]) => v !== undefined) - .map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`) - .join("&"), - ]; - } else { - return query.path; - } -}; - -export const useFetch = (query: QueryIdentifier) => { - return useQuery({ - queryKey: toQueryKey(query), - queryFn: (ctx) => queryFn(query.parser, ctx), - }); -}; - -export const useInfiniteFetch = (query: QueryIdentifier) => { - const ret = useInfiniteQuery, KyooErrors>({ - queryKey: toQueryKey(query), - queryFn: (ctx) => queryFn(Paged(query.parser), ctx), - getNextPageParam: (page: Page) => 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); -}; diff --git a/front/packages/models/src/index.ts b/front/packages/models/src/index.ts index 025a2157..d6de982a 100644 --- a/front/packages/models/src/index.ts +++ b/front/packages/models/src/index.ts @@ -18,10 +18,10 @@ * along with Kyoo. If not, see . */ +export * from "./resources"; +export * from "./traits"; export * from "./page"; export * from "./kyoo-errors"; export * from "./utils" -export * from "./traits"; -export * from "./resources"; export * from "./query"; diff --git a/front/packages/models/src/query.tsx b/front/packages/models/src/query.tsx index 74b58d75..55f0a494 100644 --- a/front/packages/models/src/query.tsx +++ b/front/packages/models/src/query.tsx @@ -29,7 +29,6 @@ import { import { z } from "zod"; import { KyooErrors } from "./kyoo-errors"; import { Page, Paged } from "./page"; -import { Library } from "./resources"; const queryFn = async ( type: z.ZodType, @@ -159,49 +158,3 @@ export const fetchQuery = async (queries: QueryIdentifier[]) => { ); return dehydrate(client); }; - -/* export const Fetch = ({ */ -/* query, */ -/* children, */ -/* }: { */ -/* query: QueryIdentifier; */ -/* 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 & { isLoading: false }) | { isLoading: true }; - -const isPage = (obj: unknown): obj is Page => - (typeof obj === "object" && obj && "items" in obj) || false; - -export const Fetch = ({ - query, - placeholderCount, - children, -}: { - query: QueryIdentifier; - placeholderCount?: number; - children: ( - item: Data extends Page ? WithLoading : WithLoading, - i: number, - ) => JSX.Element | null; -}) => { - const { data, error } = useFetch(query); - - if (error) throw error; - if (!isPage(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))} - - ); -}; diff --git a/front/packages/models/src/resources/library-item.ts b/front/packages/models/src/resources/library-item.ts index 9469e97d..6ceb1555 100644 --- a/front/packages/models/src/resources/library-item.ts +++ b/front/packages/models/src/resources/library-item.ts @@ -34,7 +34,7 @@ export enum ItemType { export const LibraryItemP = z.preprocess( (x: any) => { - x.aliases ??= []; + if (!x.aliases) x.aliases = []; return x; }, z.union([ diff --git a/front/packages/models/tsconfig.json b/front/packages/models/tsconfig.json index 32ca157b..cafc1ac7 100755 --- a/front/packages/models/tsconfig.json +++ b/front/packages/models/tsconfig.json @@ -14,7 +14,7 @@ "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-native", + "jsx": "react-jsx", "incremental": true, "baseUrl": ".", "paths": { diff --git a/front/packages/primitives/src/themes/catppuccin.ts b/front/packages/primitives/src/themes/catppuccin.ts index c1199412..274b2dfb 100644 --- a/front/packages/primitives/src/themes/catppuccin.ts +++ b/front/packages/primitives/src/themes/catppuccin.ts @@ -20,12 +20,14 @@ import { ThemeBuilder } from "./theme"; +// Ref: https://github.com/catppuccin/catppuccin export const catppuccin: ThemeBuilder = { fonts: { heading: "Pacifico", paragraph: "Poppins", }, light: { + // Catppuccin latte appbar: "#e64553", contrast: "#cdd6f4", subcontrast: "#bac2de", @@ -45,8 +47,17 @@ export const catppuccin: ThemeBuilder = { paragraph: "#5c5f77", subtext: "#6c6f85", }, + colors: { + red: "#d20f39", + green: "#40a02b", + blue: "#1e66f5", + yellow: "#df8e1d", + black: "#4c4f69", + white: "#eff1f5", + }, }, dark: { + // Catppuccin mocha appbar: "#94e2d5", contrast: "#cdd6f4", subcontrast: "#bac2de", @@ -66,5 +77,13 @@ export const catppuccin: ThemeBuilder = { paragraph: "#bac2de", subtext: "#a6adc8", }, + colors: { + red: "#f38ba8", + green: "#a6e3a1", + blue: "#89b4fa", + yellow: "#f9e2af", + black: "#11111b", + white: "#cdd6f4", + }, }, }; diff --git a/front/packages/primitives/src/themes/theme.tsx b/front/packages/primitives/src/themes/theme.tsx index 4fdc6aca..bf2e25e3 100644 --- a/front/packages/primitives/src/themes/theme.tsx +++ b/front/packages/primitives/src/themes/theme.tsx @@ -39,6 +39,14 @@ type Mode = { contrast: Property.Color; subcontrast: Property.Color; variant: Variant; + colors: { + red: Property.Color, + green: Property.Color, + blue: Property.Color, + yellow: Property.Color, + white: Property.Color, + black: Property.Color, + } }; type Variant = { diff --git a/front/packages/primitives/tsconfig.json b/front/packages/primitives/tsconfig.json index 32ca157b..cafc1ac7 100755 --- a/front/packages/primitives/tsconfig.json +++ b/front/packages/primitives/tsconfig.json @@ -14,7 +14,7 @@ "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-native", + "jsx": "react-jsx", "incremental": true, "baseUrl": ".", "paths": { diff --git a/front/packages/ui/src/fetch.tsx b/front/packages/ui/src/fetch.tsx new file mode 100644 index 00000000..d4d7b437 --- /dev/null +++ b/front/packages/ui/src/fetch.tsx @@ -0,0 +1,73 @@ +/* + * 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 . + */ + +import { Page, QueryIdentifier, useFetch, KyooErrors } from "@kyoo/models"; +import { P } from "@kyoo/primitives"; +import { View } from "react-native"; +import { useYoshiki } from "yoshiki/native"; + +export type WithLoading = (Item & { isLoading: false }) | { isLoading: true }; + +const isPage = (obj: unknown): obj is Page => + (typeof obj === "object" && obj && "items" in obj) || false; + +export const Fetch = ({ + query, + placeholderCount, + children, +}: { + query: QueryIdentifier; + placeholderCount: number; + children: ( + item: Data extends Page ? WithLoading : WithLoading, + i: number, + ) => JSX.Element | null; +}): JSX.Element | null => { + const { data, error } = useFetch(query); + + if (error) return ; + if (!data) + return ( + <>{[...Array(placeholderCount)].map((_, i) => children({ isLoading: true } as any, i))} + ); + if (!isPage(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 ErrorView = ({ error }: { error: KyooErrors }) => { + const { css } = useYoshiki(); + + return ( + theme.colors.red, + flex: 1, + alignItems: "center" + })} + > + {error.errors.map((x, i) => ( +

theme.colors.white })}> + {x} +

+ ))} +
+ ); +}; diff --git a/front/packages/ui/src/navbar/index.tsx b/front/packages/ui/src/navbar/index.tsx index 4f047e35..00755213 100644 --- a/front/packages/ui/src/navbar/index.tsx +++ b/front/packages/ui/src/navbar/index.tsx @@ -19,10 +19,11 @@ */ import useTranslation from "next-translate/useTranslation"; -import { Library, LibraryP, Page, Paged, Fetch, QueryIdentifier } from "@kyoo/models"; -import { useYoshiki } from "yoshiki/native"; +import { Library, LibraryP, Page, Paged, QueryIdentifier } from "@kyoo/models"; import { IconButton, Header, Avatar, A, ts } from "@kyoo/primitives"; +import { useYoshiki } from "yoshiki/native"; import { Text, View } from "react-native"; +import { Fetch } from "../fetch"; import { KyooLongLogo } from "./icon"; const tooltip = (tooltip: string): object => ({}); @@ -64,12 +65,13 @@ export const Navbar = () => { - + {(library, i) => !library.isLoading ? (