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"], +});