From e63e3605c652b408e89eb1e2cefce95e69b2ce3a Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 22 Jun 2025 17:39:19 +0200 Subject: [PATCH] Rework images & skeletons --- front/src/components/items/context-menus.tsx | 4 +- front/src/components/items/item-grid.tsx | 5 +- front/src/models/collection.ts | 12 +- front/src/models/entry.ts | 2 +- front/src/models/index.ts | 2 + front/src/models/movie.ts | 12 +- front/src/models/serie.ts | 12 +- front/src/models/show.ts | 5 +- front/src/models/studio.ts | 6 +- front/src/models/utils/genre.ts | 2 +- front/src/models/utils/images.ts | 6 +- front/src/models/utils/metadata.ts | 3 +- front/src/models/utils/page.ts | 14 +- front/src/models/utils/utils.ts | 2 +- front/src/models/video.ts | 2 +- front/src/primitives/constants/index.ts | 1 - front/src/primitives/image-background.tsx | 95 ++++++ front/src/primitives/image.tsx | 76 +++++ front/src/primitives/image/base-image.tsx | 44 --- front/src/primitives/image/blurhash.tsx | 43 --- front/src/primitives/image/blurhash.web.tsx | 322 ------------------- front/src/primitives/image/image.tsx | 102 ------ front/src/primitives/image/image.web.tsx | 89 ----- front/src/primitives/image/index.tsx | 170 ---------- front/src/primitives/image/sprite.web.tsx | 56 ---- front/src/primitives/index.ts | 6 +- front/src/primitives/skeleton.tsx | 120 +++---- front/src/primitives/skeleton.web.tsx | 138 -------- front/src/primitives/types.d.ts | 57 ---- front/src/providers/account-context.tsx | 5 + front/src/providers/account-provider.tsx | 2 +- front/src/providers/account-store.ts | 2 +- front/src/providers/settings.ts | 8 +- front/src/query/query.tsx | 45 ++- 34 files changed, 289 insertions(+), 1181 deletions(-) delete mode 100644 front/src/primitives/constants/index.ts create mode 100644 front/src/primitives/image-background.tsx create mode 100644 front/src/primitives/image.tsx delete mode 100644 front/src/primitives/image/base-image.tsx delete mode 100644 front/src/primitives/image/blurhash.tsx delete mode 100644 front/src/primitives/image/blurhash.web.tsx delete mode 100644 front/src/primitives/image/image.tsx delete mode 100644 front/src/primitives/image/image.web.tsx delete mode 100644 front/src/primitives/image/index.tsx delete mode 100644 front/src/primitives/image/sprite.web.tsx delete mode 100644 front/src/primitives/skeleton.web.tsx delete mode 100644 front/src/primitives/types.d.ts diff --git a/front/src/components/items/context-menus.tsx b/front/src/components/items/context-menus.tsx index a0904373..955da0e8 100644 --- a/front/src/components/items/context-menus.tsx +++ b/front/src/components/items/context-menus.tsx @@ -7,15 +7,13 @@ import type { ComponentProps } from "react"; import { useTranslation } from "react-i18next"; import { Platform } from "react-native"; import { useYoshiki } from "yoshiki/native"; -import type { Serie } from "~/models"; +import { WatchStatusV } from "~/models"; import { HR, IconButton, Menu, tooltip } from "~/primitives"; import { useAccount } from "~/providers/account-context"; import { useMutation } from "~/query"; import { watchListIcon } from "./watchlist-info"; // import { useDownloader } from "../../packages/ui/src/downloadses/ui/src/downloads"; -type WatchStatusV = NonNullable["status"]; - export const EpisodesContext = ({ type = "episode", slug, diff --git a/front/src/components/items/item-grid.tsx b/front/src/components/items/item-grid.tsx index fe9fc966..aa9e2acb 100644 --- a/front/src/components/items/item-grid.tsx +++ b/front/src/components/items/item-grid.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { type ImageStyle, Platform, View } from "react-native"; import { type Stylable, type Theme, percent, px, useYoshiki } from "yoshiki/native"; -import type { KyooImage, WatchStatusV } from "~/models"; +import type { KImage, WatchStatusV } from "~/models"; import { Link, P, @@ -15,6 +15,7 @@ import { } from "~/primitives"; import type { Layout } from "~/query"; import { ItemWatchStatus } from "./item-helpers"; +import { ItemContext } from "./context-menus"; export const ItemProgress = ({ watchPercent }: { watchPercent: number }) => { const { css } = useYoshiki("episodebox"); @@ -59,7 +60,7 @@ export const ItemGrid = ({ slug: string; name: string; subtitle: string | null; - poster: KyooImage | null; + poster: KImage | null; watchStatus: WatchStatusV | null; watchPercent: number | null; type: "movie" | "serie" | "collection"; diff --git a/front/src/models/collection.ts b/front/src/models/collection.ts index 86eacd74..c1d7448a 100644 --- a/front/src/models/collection.ts +++ b/front/src/models/collection.ts @@ -1,6 +1,6 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import { Genre } from "./utils/genre"; -import { Image } from "./utils/images"; +import { KImage } from "./utils/images"; import { Metadata } from "./utils/metadata"; import { zdate } from "./utils/utils"; @@ -24,10 +24,10 @@ export const Collection = z genres: z.array(Genre), externalId: Metadata, - poster: Image.nullable(), - thumbnail: Image.nullable(), - banner: Image.nullable(), - logo: Image.nullable(), + poster: KImage.nullable(), + thumbnail: KImage.nullable(), + banner: KImage.nullable(), + logo: KImage.nullable(), createdAt: zdate(), updatedAt: zdate(), diff --git a/front/src/models/entry.ts b/front/src/models/entry.ts index 25ddca9e..07ea0071 100644 --- a/front/src/models/entry.ts +++ b/front/src/models/entry.ts @@ -1,4 +1,4 @@ -import z from "zod"; +import { z } from "zod/v4"; export const Entry = z.object({ id: z.string(), diff --git a/front/src/models/index.ts b/front/src/models/index.ts index ce3582d3..e0a6a322 100644 --- a/front/src/models/index.ts +++ b/front/src/models/index.ts @@ -8,3 +8,5 @@ export * from "./show"; export * from "./entry"; export * from "./studio"; export * from "./video"; + +export * from "./utils/images"; diff --git a/front/src/models/movie.ts b/front/src/models/movie.ts index 3ed821eb..9c4fc36a 100644 --- a/front/src/models/movie.ts +++ b/front/src/models/movie.ts @@ -1,7 +1,7 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import { Studio } from "./studio"; import { Genre } from "./utils/genre"; -import { Image } from "./utils/images"; +import { KImage } from "./utils/images"; import { Metadata } from "./utils/metadata"; import { zdate } from "./utils/utils"; import { EmbeddedVideo } from "./video"; @@ -27,10 +27,10 @@ export const Movie = z genres: z.array(Genre), externalId: Metadata, - poster: Image.nullable(), - thumbnail: Image.nullable(), - banner: Image.nullable(), - logo: Image.nullable(), + poster: KImage.nullable(), + thumbnail: KImage.nullable(), + banner: KImage.nullable(), + logo: KImage.nullable(), trailerUrl: z.string().optional().nullable(), isAvailable: z.boolean(), diff --git a/front/src/models/serie.ts b/front/src/models/serie.ts index 7bca6f0d..85a4347e 100644 --- a/front/src/models/serie.ts +++ b/front/src/models/serie.ts @@ -1,8 +1,8 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import { Entry } from "./entry"; import { Studio } from "./studio"; import { Genre } from "./utils/genre"; -import { Image } from "./utils/images"; +import { KImage } from "./utils/images"; import { Metadata } from "./utils/metadata"; import { zdate } from "./utils/utils"; @@ -28,10 +28,10 @@ export const Serie = z runtime: z.number().nullable(), externalId: Metadata, - poster: Image.nullable(), - thumbnail: Image.nullable(), - banner: Image.nullable(), - logo: Image.nullable(), + poster: KImage.nullable(), + thumbnail: KImage.nullable(), + banner: KImage.nullable(), + logo: KImage.nullable(), trailerUrl: z.string().optional().nullable(), entriesCount: z.number().int(), diff --git a/front/src/models/show.ts b/front/src/models/show.ts index 3fcf719d..429e0da4 100644 --- a/front/src/models/show.ts +++ b/front/src/models/show.ts @@ -1,4 +1,4 @@ -import z from "zod"; +import { z } from "zod/v4"; import { Collection } from "./collection"; import { Movie } from "./movie"; import { Serie } from "./serie"; @@ -9,3 +9,6 @@ export const Show = z.union([ Collection.and(z.object({ kind: z.literal("collection") })), ]); export type Show = z.infer; + +export type WatchStatusV = NonNullable["status"]; +export const WatchStatusV = ["completed", "watching", "rewatching", "dropped", "planned"] as const; diff --git a/front/src/models/studio.ts b/front/src/models/studio.ts index 5e372d98..89eab446 100644 --- a/front/src/models/studio.ts +++ b/front/src/models/studio.ts @@ -1,5 +1,5 @@ -import { z } from "zod"; -import { Image } from "./utils/images"; +import { z } from "zod/v4"; +import { KImage } from "./utils/images"; import { Metadata } from "./utils/metadata"; import { zdate } from "./utils/utils"; @@ -7,7 +7,7 @@ export const Studio = z.object({ id: z.string(), slug: z.string(), name: z.string(), - logo: Image.nullable(), + logo: KImage.nullable(), externalId: Metadata, createdAt: zdate(), updatedAt: zdate(), diff --git a/front/src/models/utils/genre.ts b/front/src/models/utils/genre.ts index 2f0c5fdd..54ff79ae 100644 --- a/front/src/models/utils/genre.ts +++ b/front/src/models/utils/genre.ts @@ -1,4 +1,4 @@ -import z from "zod"; +import z from "zod/v4"; export const Genre = z.enum([ "action", diff --git a/front/src/models/utils/images.ts b/front/src/models/utils/images.ts index bc66ed81..8d7fc915 100644 --- a/front/src/models/utils/images.ts +++ b/front/src/models/utils/images.ts @@ -1,6 +1,6 @@ -import { z } from "zod"; +import { z } from "zod/v4"; -export const Image = z +export const KImage = z .object({ id: z.string(), source: z.string(), @@ -13,4 +13,4 @@ export const Image = z high: `/images/${x.id}?quality=high`, })); -export type Image = z.infer; +export type KImage = z.infer; diff --git a/front/src/models/utils/metadata.ts b/front/src/models/utils/metadata.ts index 1b443411..33000bfc 100644 --- a/front/src/models/utils/metadata.ts +++ b/front/src/models/utils/metadata.ts @@ -1,6 +1,7 @@ -import { z } from "zod"; +import { z } from "zod/v4"; export const Metadata = z.record( + z.string(), z.object({ dataId: z.string(), link: z.string().nullable(), diff --git a/front/src/models/utils/page.ts b/front/src/models/utils/page.ts index 46699ce2..206c0f68 100644 --- a/front/src/models/utils/page.ts +++ b/front/src/models/utils/page.ts @@ -1,20 +1,16 @@ -import { z } from "zod"; +import { type ZodType, z } from "zod/v4"; -export interface Page { +export type Page = { this: string; - first: string; next: string | null; - count: number; items: T[]; -} +}; -export const Paged = (item: z.ZodType): z.ZodSchema> => +export const Paged = (ItemParser: Parser) => z.object({ this: z.string(), - first: z.string(), next: z.string().nullable(), - count: z.number(), - items: z.array(item), + items: z.array(ItemParser), }); export const isPage = (obj: unknown): obj is Page => diff --git a/front/src/models/utils/utils.ts b/front/src/models/utils/utils.ts index d85e7087..bb37184d 100644 --- a/front/src/models/utils/utils.ts +++ b/front/src/models/utils/utils.ts @@ -1,3 +1,3 @@ -import { z } from "zod"; +import { z } from "zod/v4"; export const zdate = z.coerce.date; diff --git a/front/src/models/video.ts b/front/src/models/video.ts index 1cf02250..b1370907 100644 --- a/front/src/models/video.ts +++ b/front/src/models/video.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; export const EmbeddedVideo = z.object({ id: z.string(), diff --git a/front/src/primitives/constants/index.ts b/front/src/primitives/constants/index.ts deleted file mode 100644 index 8f70f471..00000000 --- a/front/src/primitives/constants/index.ts +++ /dev/null @@ -1 +0,0 @@ -export const imageBorderRadius = 10; diff --git a/front/src/primitives/image-background.tsx b/front/src/primitives/image-background.tsx new file mode 100644 index 00000000..727d790e --- /dev/null +++ b/front/src/primitives/image-background.tsx @@ -0,0 +1,95 @@ +import { ImageBackground as EImageBackground } from "expo-image"; +import { LinearGradient, type LinearGradientProps } from "expo-linear-gradient"; +import type { ComponentProps, ReactNode } from "react"; +import type { ImageStyle } from "react-native"; +import { useYoshiki } from "yoshiki/native"; +import type { KImage } from "~/models"; +import { useToken } from "~/providers/account-context"; +import type { ImageLayout, YoshikiEnhanced } from "./image"; + +// This should stay in think with `Image`. +// (copy-pasted but change `EImage` with `EImageBackground`) +// ALSO, remove `border-radius` (it's weird otherwise) +export const ImageBackground = ({ + src, + quality, + alt, + layout, + ...props +}: { + src: KImage | null; + quality: "low" | "medium" | "high"; + alt?: string; + style?: ImageStyle; + layout: ImageLayout; + children: ReactNode; +}) => { + const { css } = useYoshiki(); + const { apiUrl, authToken } = useToken(); + + return ( + + ); +}; +export const PosterBackground = ({ + alt, + layout, + ...props +}: Omit, "layout"> & { + style?: ImageStyle; + layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>; +}) => { + const { css } = useYoshiki(); + + return ( + + ); +}; + +export const GradientImageBackground = ({ + gradient, + children, + ...props +}: ComponentProps & { + gradient?: Partial; +}) => { + const { css, theme } = useYoshiki(); + + return ( + + + {children} + + + ); +}; diff --git a/front/src/primitives/image.tsx b/front/src/primitives/image.tsx new file mode 100644 index 00000000..7663dc91 --- /dev/null +++ b/front/src/primitives/image.tsx @@ -0,0 +1,76 @@ +import { Image as EImage } from "expo-image"; +import type { ComponentProps } from "react"; +import type { ImageStyle, ViewStyle } from "react-native"; +import { useYoshiki } from "yoshiki/native"; +import type { YoshikiStyle } from "yoshiki/src/type"; +import type { KImage } from "~/models"; +import { useToken } from "~/providers/account-context"; +import { Skeleton } from "./skeleton"; + +export type YoshikiEnhanced -); - export const Skeleton = ({ - children, - show: forcedShow, lines = 1, variant = "text", ...props }: Omit & { - children?: JSX.Element | JSX.Element[] | boolean | null; - show?: boolean; lines?: number; variant?: "text" | "header" | "round" | "custom" | "fill" | "filltext"; }) => { @@ -75,8 +35,6 @@ export const Skeleton = ({ mult.value = withRepeat(withDelay(800, withTiming(1, { duration: 800 })), 0); }); - if (forcedShow === undefined && children && children !== true) return <>{children}; - return ( - {(forcedShow || !children || children === true) && - [...Array(lines)].map((_, i) => ( - { - width.value = e.nativeEvent.layout.width; - }} - {...css([ - { - bg: (theme) => theme.overlay0, - }, - lines === 1 && { - position: "absolute", - top: 0, - bottom: 0, - left: 0, - right: 0, - }, - lines !== 1 && { - width: i === lines - 1 ? percent(40) : percent(100), - height: rem(1.2), - marginBottom: rem(0.5), - overflow: "hidden", - borderRadius: px(6), - }, - ])} - > - - - ))} - {children} + {[...Array(lines)].map((_, i) => ( + { + if (i === 0) width.value = e.nativeEvent.layout.width; + }} + {...css([ + { + bg: (theme) => theme.overlay0, + }, + lines === 1 && { + position: "absolute", + top: 0, + bottom: 0, + left: 0, + right: 0, + }, + lines !== 1 && { + width: i === lines - 1 ? percent(40) : percent(100), + height: rem(1.2), + marginBottom: rem(0.5), + overflow: "hidden", + borderRadius: px(6), + }, + ])} + > + + + ))} ); }; diff --git a/front/src/primitives/skeleton.web.tsx b/front/src/primitives/skeleton.web.tsx deleted file mode 100644 index 318e9aa7..00000000 --- a/front/src/primitives/skeleton.web.tsx +++ /dev/null @@ -1,138 +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 { LinearGradient } from "expo-linear-gradient"; -import { View, type ViewProps } from "react-native"; -import { em, percent, px, rem, useYoshiki } from "yoshiki/native"; - -export const SkeletonCss = () => ( - -); - -export const Skeleton = ({ - children, - show: forcedShow, - lines = 1, - variant = "text", - ...props -}: Omit & { - children?: JSX.Element | JSX.Element[] | boolean | null; - show?: boolean; - lines?: number; - variant?: "text" | "header" | "round" | "custom" | "fill" | "filltext"; -}) => { - const { css, theme } = useYoshiki(); - - if (forcedShow === undefined && children && children !== true) return <>{children}; - - return ( - - {(forcedShow || !children || children === true) && - [...Array(lines)].map((_, i) => ( - theme.overlay0, - }, - lines === 1 && { - position: "absolute", - top: 0, - bottom: 0, - left: 0, - right: 0, - }, - lines !== 1 && { - width: i === lines - 1 ? percent(40) : percent(100), - height: rem(1.2), - marginBottom: rem(0.5), - overflow: "hidden", - borderRadius: px(6), - }, - ])} - > - - - ))} - {children} - - ); -}; diff --git a/front/src/primitives/types.d.ts b/front/src/primitives/types.d.ts deleted file mode 100644 index 005ec2cc..00000000 --- a/front/src/primitives/types.d.ts +++ /dev/null @@ -1,57 +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 type React from "react"; -import "react-native"; - -declare module "react-native" { - interface PressableStateCallbackType { - hovered?: boolean; - focused?: boolean; - } - interface AccessibilityProps { - tabIndex?: number; - } - interface ViewStyle { - transitionProperty?: string; - transitionDuration?: string; - } - interface TextProps { - hrefAttrs?: { - rel?: "noreferrer"; - target?: string; - }; - } - interface ViewProps { - dataSet?: Record; - hrefAttrs?: { - rel: "noreferrer"; - target?: "_blank"; - }; - onClick?: (e: React.MouseEvent) => void; - } -} - -declare module "react" { - interface StyleHTMLAttributes extends HTMLAttributes { - jsx?: boolean; - global?: boolean; - } -} diff --git a/front/src/providers/account-context.tsx b/front/src/providers/account-context.tsx index 4bb6e8da..c3871bec 100644 --- a/front/src/providers/account-context.tsx +++ b/front/src/providers/account-context.tsx @@ -9,6 +9,11 @@ export const AccountContext = createContext<{ accounts: (Account & { select: () => void; remove: () => void })[]; }>({ apiUrl: "api", authToken: null, selectedAccount: null, accounts: [] }); +export const useToken = () => { + const { apiUrl, authToken } = useContext(AccountContext); + return { apiUrl, authToken }; +}; + export const useAccount = () => { const { selectedAccount } = useContext(AccountContext); return selectedAccount; diff --git a/front/src/providers/account-provider.tsx b/front/src/providers/account-provider.tsx index 2f3fd326..5e72cec3 100644 --- a/front/src/providers/account-provider.tsx +++ b/front/src/providers/account-provider.tsx @@ -1,7 +1,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { type ReactNode, useEffect, useMemo, useRef } from "react"; import { Platform } from "react-native"; -import { z } from "zod"; +import { z } from "zod/v4"; import { AccountP, UserP } from "~/models"; import { useFetch } from "~/query"; import { AccountContext } from "./account-context"; diff --git a/front/src/providers/account-store.ts b/front/src/providers/account-store.ts index 32a02224..44dfc7b8 100644 --- a/front/src/providers/account-store.ts +++ b/front/src/providers/account-store.ts @@ -1,5 +1,5 @@ import { Platform } from "react-native"; -import { z } from "zod"; +import { z } from "zod/v4"; import { type Account, AccountP } from "~/models"; import { readValue, setCookie, storeValue } from "./settings"; diff --git a/front/src/providers/settings.ts b/front/src/providers/settings.ts index 3b0e5210..f8f7ca2c 100644 --- a/front/src/providers/settings.ts +++ b/front/src/providers/settings.ts @@ -1,6 +1,6 @@ import { Platform } from "react-native"; import { MMKV, useMMKVString } from "react-native-mmkv"; -import type { ZodTypeAny, z } from "zod"; +import type { z, ZodType } from "zod/v4"; import { getServerData } from "~/utils"; export const storage = new MMKV(); @@ -24,7 +24,7 @@ export const setCookie = (key: string, val?: unknown) => { document.cookie = `${key}=${value};${expires};path=/;samesite=strict`; }; -export const readCookie = (key: string, parser: T) => { +export const readCookie = (key: string, parser: T) => { const cookies = getServerData("cookies"); console.log("cookies", cookies); const decodedCookie = decodeURIComponent(cookies); @@ -37,7 +37,7 @@ export const readCookie = (key: string, parser: T) => { return parser.parse(JSON.parse(str)) as z.infer; }; -export const useStoreValue = (key: string, parser: T) => { +export const useStoreValue = (key: string, parser: T) => { if (Platform.OS === "web" && typeof window === "undefined") { return readCookie(key, parser); } @@ -50,7 +50,7 @@ export const storeValue = (key: string, value: unknown) => { storage.set(key, JSON.stringify(value)); }; -export const readValue = (key: string, parser: T) => { +export const readValue = (key: string, parser: T) => { if (Platform.OS === "web" && typeof window === "undefined") { return readCookie(key, parser); } diff --git a/front/src/query/query.tsx b/front/src/query/query.tsx index 00dfacb4..c731bd52 100644 --- a/front/src/query/query.tsx +++ b/front/src/query/query.tsx @@ -1,6 +1,6 @@ import { - QueryClient, dehydrate, + QueryClient, useInfiniteQuery, useQuery, useQueryClient, @@ -8,12 +8,12 @@ import { } from "@tanstack/react-query"; import { useContext } from "react"; import { Platform } from "react-native"; -import type { z } from "zod"; +import type { z } from "zod/v4"; import { type KyooError, type Page, Paged } from "~/models"; import { AccountContext } from "~/providers/account-context"; import { setServerData } from "~/utils"; -const ssrApiUrl = process.env.KYOO_URL ?? "http://back/api"; +const ssrApiUrl = process.env.KYOO_URL ?? "http://api:3567/api"; const cleanSlash = (str: string | null, keepFirst = false) => { if (!str) return null; @@ -71,9 +71,9 @@ const queryFn = async (context: { throw data as KyooError; } - if (resp.status === 204) return null; + if (resp.status === 204) return null!; - if (context.plainText) return (await resp.text()) as unknown; + if (context.plainText) return (await resp.text()) as any; let data: Record; try { @@ -82,7 +82,7 @@ const queryFn = async (context: { console.error("Invalid json from kyoo", e); throw { message: "Invalid response from kyoo", status: "json" } as KyooError; } - if (!context.parser) return data; + if (!context.parser) return data as any; const parsed = await context.parser.safeParseAsync(data); if (!parsed.success) { console.log("Url: ", context.url, " Response: ", resp.status, " Parse error: ", parsed.error); @@ -108,11 +108,11 @@ export const createQueryClient = () => }, }); -export type QueryIdentifier = { - parser: z.ZodType; +export type QueryIdentifier = { + parser: z.ZodType; path: (string | undefined)[]; params?: { [query: string]: boolean | number | string | string[] | undefined }; - infinite?: boolean | { value: true; map?: (x: any[]) => Ret[] }; + infinite?: boolean; placeholderData?: T | (() => T); enabled?: boolean; @@ -143,7 +143,8 @@ export const keyToUrl = (key: ReturnType) => { }; export const useFetch = (query: QueryIdentifier) => { - const { apiUrl, authToken } = useContext(AccountContext); + let { apiUrl, authToken } = useContext(AccountContext); + if (query.options?.apiUrl) apiUrl = query.options.apiUrl; const key = toQueryKey({ apiUrl, path: query.path, params: query.params }); return useQuery({ @@ -155,17 +156,18 @@ export const useFetch = (query: QueryIdentifier) => { signal: ctx.signal, authToken: authToken ?? null, ...query.options, - }), + }) as Promise, placeholderData: query.placeholderData as any, enabled: query.enabled, }); }; -export const useInfiniteFetch = (query: QueryIdentifier) => { - const { apiUrl, authToken } = useContext(AccountContext); +export const useInfiniteFetch = (query: QueryIdentifier) => { + let { apiUrl, authToken } = useContext(AccountContext); + if (query.options?.apiUrl) apiUrl = query.options.apiUrl; const key = toQueryKey({ apiUrl, path: query.path, params: query.params }); - const ret = useInfiniteQuery, KyooError>({ + const res = useInfiniteQuery, KyooError>({ queryKey: key, queryFn: (ctx) => queryFn({ @@ -174,20 +176,15 @@ export const useInfiniteFetch = (query: QueryIdentifier) = signal: ctx.signal, authToken: authToken ?? null, ...query.options, - }), + }) as Promise>, getNextPageParam: (page: Page) => page?.next || undefined, initialPageParam: undefined, placeholderData: query.placeholderData as any, enabled: query.enabled, }); - const items = ret.data?.pages.flatMap((x) => x.items); - return { - ...ret, - items: - items && typeof query.infinite === "object" && query.infinite.map - ? query.infinite.map(items) - : (items as unknown as Ret[] | undefined), - }; + const ret = res as typeof res & { items?: Data[] }; + ret.items = ret.data?.pages.flatMap((x) => x.items); + return ret; }; export const prefetch = async (...queries: QueryIdentifier[]) => { @@ -242,7 +239,7 @@ type MutationParams = { body?: object; }; -export const useMutation = ({ +export const useMutation = ({ compute, invalidate, ...queryParams