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 ? (